feat(desktop): use collective mesh routing for Auto (#2825)

Uses MeshLLM built-in `mesh` collective intelligence / Mixture of Agents
when Buzz Auto sees two or more distinct physical models. With zero or
one model, Auto remains ordinary `auto`. This is an alternative way to
improve tool responses, accuracy, and resistance to hallucination when a
high-latency distributed mesh contains diverse models.

Models and members may come and go: collective routing enables only
after stable capacity, drops on confirmed contraction, and can recover
later. Mesh-specific failures retry once through ordinary Auto.

This update also pins MeshLLM to a v0.73.1-compatible backport of
[MeshLLM #1074](https://github.com/Mesh-LLM/mesh-llm/pull/1074), so
client-only Buzz nodes cannot enter model election or download a remote
provider model. Buzz preserves the selected local sharing model and
switches an existing client to sharing across a controlled app restart,
retaining one runtime and one `:9337` / `:3131` pair per machine.

Validation:
- Full local `just ci` passes on the cleaned branch.
- MeshLLM host-runtime suite: 1,568 passed, 0 failed; strict Clippy
passes.
- Buzz desktop Tauri suite with `mesh-llm`: 1,721 passed, 0 failed;
strict feature Clippy passes.
- Playwright covers client-to-share using the saved local model and no
destructive stop.
- Packaged two-machine testing proved single-model routing, dual-model
collective routing, tool-markup fallback, and runtime reuse.
- Packaged client-only recheck routed a real Mini Buzz turn through M5
while Mini stayed `is_client=true`, `is_host=false`, hosted no models,
and created no Gemma cache.

Builds on the recovery work merged in #2823; this PR does not duplicate
it.

---------

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: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
Mic Neale
2026-07-27 09:46:13 +10:00
committed by GitHub
parent aee6314484
commit 16d4ec335e
22 changed files with 2304 additions and 609 deletions

View File

@@ -726,6 +726,12 @@ pub struct Config {
pub anthropic_api_version: String,
/// OpenAI endpoint selection. See [`OpenAiApi`].
pub openai_api: OpenAiApi,
/// Prefer mesh-llm's virtual `mesh` model when the configured/effective
/// OpenAI model is `auto` and the live model catalog advertises it.
/// Set by Buzz's relay-mesh provider via
/// `BUZZ_AGENT_PREFER_MESH_FOR_AUTO=1`; other providers keep their
/// existing `auto` semantics.
pub prefer_mesh_for_auto: bool,
pub hints_enabled: bool,
/// Thinking/reasoning effort level. `None` = use provider default (no
/// thinking config sent). Set via `BUZZ_AGENT_THINKING_EFFORT`.
@@ -798,6 +804,7 @@ impl Config {
base_url,
anthropic_api_version: env_or("ANTHROPIC_API_VERSION", "2023-06-01"),
openai_api,
prefer_mesh_for_auto: parse_env("BUZZ_AGENT_PREFER_MESH_FOR_AUTO", 0u8)? != 0,
max_rounds: parse_env("BUZZ_AGENT_MAX_ROUNDS", 0)?,
max_output_tokens: parse_env("BUZZ_AGENT_MAX_OUTPUT_TOKENS", 32_768)?,
llm_timeout: Duration::from_secs(parse_env("BUZZ_AGENT_LLM_TIMEOUT_SECS", 240)?),
@@ -844,6 +851,7 @@ impl Config {
system_prompt: String::new(),
anthropic_api_version: "2023-06-01".into(),
openai_api: OpenAiApi::Chat,
prefer_mesh_for_auto: false,
max_rounds: 0,
max_output_tokens: 1,
llm_timeout: Duration::from_secs(30),

File diff suppressed because it is too large Load Diff

View File

@@ -4935,7 +4935,7 @@ dependencies = [
[[package]]
name = "mesh-llm-api-client"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"hex",
"mesh-llm-client",
@@ -4945,7 +4945,7 @@ dependencies = [
[[package]]
name = "mesh-llm-api-server"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"mesh-llm-api-client",
@@ -4956,12 +4956,12 @@ dependencies = [
[[package]]
name = "mesh-llm-build-info"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
[[package]]
name = "mesh-llm-client"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"async-trait",
@@ -4993,7 +4993,7 @@ dependencies = [
[[package]]
name = "mesh-llm-config"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"dirs",
@@ -5009,7 +5009,7 @@ dependencies = [
[[package]]
name = "mesh-llm-embedded-runtime"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"mesh-llm-host-runtime",
@@ -5019,7 +5019,7 @@ dependencies = [
[[package]]
name = "mesh-llm-events"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"clap",
@@ -5031,7 +5031,7 @@ dependencies = [
[[package]]
name = "mesh-llm-gpu-bench"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"cc",
@@ -5044,7 +5044,7 @@ dependencies = [
[[package]]
name = "mesh-llm-guardrails"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"serde",
"serde_json",
@@ -5053,7 +5053,7 @@ dependencies = [
[[package]]
name = "mesh-llm-hardware-profile"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"mesh-llm-native-runtime",
]
@@ -5061,7 +5061,7 @@ dependencies = [
[[package]]
name = "mesh-llm-host-runtime"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"argon2",
@@ -5154,7 +5154,7 @@ dependencies = [
[[package]]
name = "mesh-llm-identity"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"argon2",
"base64 0.22.1",
@@ -5176,7 +5176,7 @@ dependencies = [
[[package]]
name = "mesh-llm-native-runtime"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"serde",
@@ -5187,7 +5187,7 @@ dependencies = [
[[package]]
name = "mesh-llm-node"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"mesh-llm-types",
@@ -5201,7 +5201,7 @@ dependencies = [
[[package]]
name = "mesh-llm-plugin"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"async-trait",
@@ -5218,7 +5218,7 @@ dependencies = [
[[package]]
name = "mesh-llm-plugin-manager"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"dirs",
@@ -5236,7 +5236,7 @@ dependencies = [
[[package]]
name = "mesh-llm-protocol"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"hex",
@@ -5249,7 +5249,7 @@ dependencies = [
[[package]]
name = "mesh-llm-routing"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"iroh",
]
@@ -5257,7 +5257,7 @@ dependencies = [
[[package]]
name = "mesh-llm-runtime-install"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"dirs",
@@ -5280,7 +5280,7 @@ dependencies = [
[[package]]
name = "mesh-llm-sdk"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"mesh-llm-api-client",
@@ -5295,7 +5295,7 @@ dependencies = [
[[package]]
name = "mesh-llm-skills"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"dirs",
@@ -5306,7 +5306,7 @@ dependencies = [
[[package]]
name = "mesh-llm-system"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"chrono",
@@ -5329,7 +5329,7 @@ dependencies = [
[[package]]
name = "mesh-llm-types"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"hex",
"serde",
@@ -5340,12 +5340,12 @@ dependencies = [
[[package]]
name = "mesh-llm-ui"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
[[package]]
name = "mesh-mixture-of-agents"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"async-trait",
"mesh-llm-guardrails",
@@ -5418,7 +5418,7 @@ dependencies = [
[[package]]
name = "model-artifact"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"async-trait",
@@ -5429,7 +5429,7 @@ dependencies = [
[[package]]
name = "model-hf"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"async-trait",
@@ -5447,7 +5447,7 @@ dependencies = [
[[package]]
name = "model-package"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"bytes",
@@ -5467,7 +5467,7 @@ dependencies = [
[[package]]
name = "model-ref"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"serde",
]
@@ -5475,7 +5475,7 @@ dependencies = [
[[package]]
name = "model-resolver"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"model-artifact",
@@ -6504,7 +6504,7 @@ dependencies = [
[[package]]
name = "openai-frontend"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"async-trait",
"axum",
@@ -9037,7 +9037,7 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]]
name = "skippy-cache"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"blake3",
@@ -9047,7 +9047,7 @@ dependencies = [
[[package]]
name = "skippy-coordinator"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"thiserror 2.0.18",
]
@@ -9055,7 +9055,7 @@ dependencies = [
[[package]]
name = "skippy-ffi"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"libloading 0.8.9",
]
@@ -9063,12 +9063,12 @@ dependencies = [
[[package]]
name = "skippy-metrics"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
[[package]]
name = "skippy-protocol"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"prost",
"prost-build",
@@ -9079,7 +9079,7 @@ dependencies = [
[[package]]
name = "skippy-runtime"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"libc",
@@ -9093,7 +9093,7 @@ dependencies = [
[[package]]
name = "skippy-server"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"anyhow",
"async-trait",
@@ -9121,7 +9121,7 @@ dependencies = [
[[package]]
name = "skippy-topology"
version = "0.73.1"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
dependencies = [
"serde",
"serde_json",

View File

@@ -90,14 +90,14 @@ buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona
buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" }
iroh = { version = "1.0.2", optional = true }
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
# Model catalog + hardware survey for the Share-compute model picker (same
# diagnose pattern as mesh-console). Lib name of mesh-llm-client is mesh_client.
mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-client", optional = true }
mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-node", optional = true }
mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-system", optional = true }
mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-events", optional = true }
mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-client", optional = true }
mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-node", optional = true }
mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-system", optional = true }
mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-events", optional = true }
base64 = "0.22"
sha2 = "0.11"
tar = "0.4"

View File

@@ -1,5 +1,6 @@
use std::path::PathBuf;
use sha2::{Digest, Sha256};
use tauri::{AppHandle, Manager, State};
use crate::{app_state::AppState, mesh_llm, relay};
@@ -57,8 +58,85 @@ fn share_stop_should_teardown(mode: mesh_llm::MeshNodeMode) -> bool {
matches!(mode, mesh_llm::MeshNodeMode::Serve)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeshStartPlan {
Start,
RestartToReplaceClient,
RejectOccupied,
}
fn mesh_start_plan(
requested_mode: mesh_llm::MeshNodeMode,
existing_mode: Option<mesh_llm::MeshNodeMode>,
) -> MeshStartPlan {
match (requested_mode, existing_mode) {
(_, None) => MeshStartPlan::Start,
(mesh_llm::MeshNodeMode::Serve, Some(mesh_llm::MeshNodeMode::Client)) => {
MeshStartPlan::RestartToReplaceClient
}
_ => MeshStartPlan::RejectOccupied,
}
}
fn sharing_config_from_request(
request: &mesh_llm::StartMeshNodeRequest,
) -> CmdResult<MeshSharingConfig> {
let model_id = request
.model_id
.as_deref()
.map(str::trim)
.filter(|model_id| !model_id.is_empty())
.ok_or_else(|| "modelId is required for serve mode".to_string())?;
Ok(MeshSharingConfig {
enabled: true,
model_id: model_id.to_string(),
max_vram_gb: request.max_vram_gb,
})
}
fn restarting_share_status(config: &MeshSharingConfig) -> mesh_llm::MeshNodeStatus {
mesh_llm::MeshNodeStatus {
state: mesh_llm::MeshNodeState::Starting,
mode: Some(mesh_llm::MeshNodeMode::Serve),
health: mesh_llm::MeshHealth {
status: mesh_llm::MeshHealthStatus::Degraded,
reason: Some("Buzz is restarting to switch this machine to sharing".to_string()),
},
api_base_url: None,
console_url: None,
model_id: Some(config.model_id.clone()),
model_name: Some(config.model_id.clone()),
invite_token: None,
endpoint_id: None,
device_id: None,
device_name: None,
}
}
fn restart_to_share(
app: &AppHandle,
config: &MeshSharingConfig,
) -> CmdResult<mesh_llm::MeshNodeStatus> {
save_mesh_sharing_config(app, config)?;
let status = restarting_share_status(config);
app.request_restart();
Ok(status)
}
pub type CmdResult<T> = Result<T, String>;
fn buzz_mesh_name_for_relay(relay_url: &str) -> String {
let normalized = url::Url::parse(relay_url.trim())
.map(|url| url.origin().ascii_serialization())
.unwrap_or_else(|_| relay_url.trim().trim_end_matches('/').to_ascii_lowercase());
let digest = hex::encode(Sha256::digest(normalized.as_bytes()));
format!("buzz-community-{}", &digest[..32])
}
fn buzz_mesh_name(state: &AppState) -> String {
buzz_mesh_name_for_relay(&relay::relay_ws_url_with_override(state))
}
fn advance_mesh_status_cursor(
filter: &mut serde_json::Value,
page: &[nostr::Event],
@@ -137,6 +215,81 @@ pub(crate) async fn resolve_trusted_owner_ids_or_self_only(state: &AppState) ->
}
}
/// Choose validated live endpoints from other runtimes in this Buzz community.
/// The stable relay-derived mesh name gives every runtime the same MeshLLM mesh
/// identity; these endpoints supply transport bootstrap only.
fn buzz_mesh_join_targets(
mut targets: Vec<mesh_llm::MeshServeTarget>,
self_owner_id: &str,
) -> Vec<mesh_llm::MeshServeTarget> {
targets.retain(|target| {
target.reporter_pubkey.is_some()
&& target
.owner_id
.as_deref()
.is_some_and(|owner| !owner.eq_ignore_ascii_case(self_owner_id.trim()))
});
targets.sort_by(|left, right| {
left.reporter_pubkey
.cmp(&right.reporter_pubkey)
.then_with(|| left.owner_id.cmp(&right.owner_id))
.then_with(|| left.endpoint_id.cmp(&right.endpoint_id))
.then_with(|| left.endpoint_addr.cmp(&right.endpoint_addr))
.then_with(|| left.model_id.cmp(&right.model_id))
});
targets.dedup_by(|left, right| left.endpoint_addr == right.endpoint_addr);
targets
}
/// Resolve the validated member endpoint this runtime should join to enter the
/// existing Buzz community mesh. `Ok(None)` means this machine is the first
/// live serving member (or is itself the shared bootstrap contact).
pub(crate) async fn resolve_buzz_mesh_join_targets(
state: &AppState,
) -> Result<Vec<mesh_llm::MeshServeTarget>, String> {
let events = query_mesh_discovery_events(state).await?;
let self_owner_id = mesh_llm::ensure_owner_identity()
.map_err(|error| format!("failed to load mesh owner identity: {error}"))?
.owner_id;
Ok(buzz_mesh_join_targets(
mesh_llm::availability_from_events(events).serve_targets,
&self_owner_id,
))
}
/// Resolve the initial admission roster and bootstrap endpoint from one relay
/// snapshot. A node start used to repeat the full membership + status query
/// for each value, making Share Compute startup both slower and more exposed
/// to inconsistent snapshots.
async fn resolve_buzz_mesh_startup(state: &AppState) -> (Vec<String>, Option<String>) {
match query_mesh_discovery_events(state).await {
Ok(events) => {
let trusted_owner_ids = mesh_llm::owner_ids_from_events(&events);
let join_token = mesh_llm::ensure_owner_identity()
.ok()
.and_then(|identity| {
buzz_mesh_join_targets(
mesh_llm::availability_from_events(events).serve_targets,
&identity.owner_id,
)
.into_iter()
.next()
})
.map(|target| target.endpoint_addr);
(trusted_owner_ids, join_token)
}
Err(error) => {
// Initial startup fails closed to this runtime's own owner. Share
// Compute must still start for the first member and through a
// transient relay outage; the coordinator retries convergence.
eprintln!(
"buzz-mesh: startup discovery failed; allowing only this node and starting isolated for now: {error}"
);
(Vec::new(), None)
}
}
}
pub(crate) async fn restore_mesh_sharing(app: &AppHandle, state: &AppState) -> CmdResult<()> {
let Some(config) = load_mesh_sharing_config(app)? else {
return Ok(());
@@ -144,6 +297,10 @@ pub(crate) async fn restore_mesh_sharing(app: &AppHandle, state: &AppState) -> C
if !config.enabled || config.model_id.trim().is_empty() {
return Ok(());
}
if state.mesh_llm_runtime.lock().await.is_some() {
return Ok(());
}
let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup(state).await;
let mut runtime = state.mesh_llm_runtime.lock().await;
if runtime.is_some() {
return Ok(());
@@ -152,8 +309,9 @@ pub(crate) async fn restore_mesh_sharing(app: &AppHandle, state: &AppState) -> C
mode: mesh_llm::MeshNodeMode::Serve,
model_id: Some(config.model_id),
max_vram_gb: config.max_vram_gb,
join_token: None,
trusted_owner_ids: Some(resolve_trusted_owner_ids_or_self_only(state).await),
join_token,
mesh_name: Some(buzz_mesh_name(state)),
trusted_owner_ids: Some(trusted_owner_ids),
};
let started = mesh_llm::DesktopMeshRuntime::start(request)
.await
@@ -170,37 +328,91 @@ pub async fn mesh_start_node(
state: State<'_, AppState>,
mut request: mesh_llm::StartMeshNodeRequest,
) -> CmdResult<mesh_llm::MeshNodeStatus> {
// Frontend requests never carry a roster; resolve it here so every
// UI-started node enforces the member allowlist.
if request.trusted_owner_ids.is_none() {
request.trusted_owner_ids = Some(resolve_trusted_owner_ids_or_self_only(&state).await);
let sharing_config = if request.mode == mesh_llm::MeshNodeMode::Serve {
Some(sharing_config_from_request(&request)?)
} else {
None
};
// Never replace a client runtime in-process. Even a Ready SDK handle can
// finish `stop()` while its native listeners are still releasing :9337
// and :3131; a pending client has no shutdown handle at all. Persist the
// requested serving configuration and switch roles across a controlled
// process restart, the only boundary that proves both ports are clean.
{
let runtime = state.mesh_llm_runtime.lock().await;
if let Some(existing) = runtime.as_ref() {
let plan = mesh_start_plan(request.mode, Some(existing.mode()));
match plan {
MeshStartPlan::RestartToReplaceClient => {
let config = sharing_config
.as_ref()
.ok_or_else(|| "serving configuration is unavailable".to_string())?;
drop(runtime);
return restart_to_share(&app, config);
}
MeshStartPlan::RejectOccupied => {
return Err("mesh node is already running".to_string());
}
MeshStartPlan::Start => {}
}
}
}
// Frontend requests never carry a roster. Resolve it and the bootstrap
// endpoint from one snapshot so UI startup does not repeat relay probes.
if request.trusted_owner_ids.is_none() || request.join_token.is_none() {
let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup(&state).await;
request.trusted_owner_ids.get_or_insert(trusted_owner_ids);
if request.join_token.is_none() {
request.join_token = join_token;
}
}
request.mesh_name = Some(buzz_mesh_name(&state));
let mut runtime = state.mesh_llm_runtime.lock().await;
if runtime.is_some() {
let plan = match runtime.as_ref() {
Some(existing) => mesh_start_plan(request.mode, Some(existing.mode())),
None => mesh_start_plan(request.mode, None),
};
if plan == MeshStartPlan::RestartToReplaceClient {
let config = sharing_config
.as_ref()
.ok_or_else(|| "serving configuration is unavailable".to_string())?;
drop(runtime);
return restart_to_share(&app, config);
}
if plan == MeshStartPlan::RejectOccupied {
return Err("mesh node is already running".to_string());
}
let saved_request = request.clone();
let started = mesh_llm::DesktopMeshRuntime::start(request)
.await
.map_err(|error| format!("{error:#}"))?;
let status = started
.status()
.await
.map_err(|error| format!("mesh node started but status probe failed: {error:#}"))?;
let status = match started.status().await {
Ok(status) => status,
Err(error) => {
let cleanup = started.stop().await;
if let Err(cleanup_error) = &cleanup {
eprintln!(
"buzz-mesh: started node status failed and cleanup was incomplete: {cleanup_error:#}"
);
}
// The handle was never installed into AppState, so shutdown cannot
// see it again. Restart even when stop reported success: the
// process boundary guarantees native :9337/:3131 listeners cannot
// linger behind an untracked runtime.
drop(runtime);
app.request_restart();
return Err(format!(
"mesh node started but status probe failed: {error:#}; Buzz is restarting to guarantee cleanup"
));
}
};
*runtime = Some(started);
drop(runtime);
if saved_request.mode == mesh_llm::MeshNodeMode::Serve {
if let Some(model_id) = saved_request.model_id.as_deref() {
save_mesh_sharing_config(
&app,
&MeshSharingConfig {
enabled: true,
model_id: model_id.to_string(),
max_vram_gb: saved_request.max_vram_gb,
},
)?;
}
if let Some(config) = sharing_config.as_ref() {
save_mesh_sharing_config(&app, config)?;
}
mesh_llm::publish_current_status_once(&app, "start").await;
Ok(status)
@@ -398,12 +610,21 @@ pub(crate) async fn ensure_client_node_for_model(
mode: mesh_llm::MeshNodeMode::Client,
model_id: None,
max_vram_gb: None,
join_token: Some(join_token),
join_token: Some(join_token.clone()),
mesh_name: Some(buzz_mesh_name(state)),
trusted_owner_ids: Some(resolve_trusted_owner_ids_or_self_only(state).await),
};
let mut runtime = state.mesh_llm_runtime.lock().await;
if runtime.is_some() {
return Err("mesh node changed while starting Buzz shared compute client".to_string());
if let Some(existing) = runtime.as_ref() {
// Another GUI agent may have won the startup race while this caller
// was resolving membership. The runtime is machine-scoped, not
// agent-scoped: join its selected endpoint into the existing node and
// let every caller reuse the same local ingress.
existing
.dial_endpoint_addr(join_token)
.await
.map_err(|error| format!("mesh dial failed: {error}"))?;
return existing.status().await.map_err(|error| error.to_string());
}
let started = mesh_llm::DesktopMeshRuntime::start(start)
.await
@@ -644,266 +865,5 @@ pub async fn mesh_model_catalog() -> CmdResult<mesh_llm::MeshModelCatalog> {
}
#[cfg(all(test, feature = "mesh-llm"))]
mod tests {
use super::*;
use crate::app_state::build_app_state;
fn target(model_id: &str, endpoint_addr: &str) -> mesh_llm::MeshServeTarget {
mesh_llm::MeshServeTarget {
model_id: model_id.to_string(),
model_name: None,
endpoint_addr: endpoint_addr.to_string(),
node_name: None,
capacity: None,
endpoint_id: None,
device_id: None,
device_name: None,
}
}
#[test]
fn readiness_failure_is_catalog_sync_when_model_never_visible() {
assert_eq!(
classify_mesh_readiness_failure(false),
MeshReadinessFailure::CatalogNeverSynced
);
}
#[test]
fn readiness_failure_is_routing_when_model_was_visible() {
assert_eq!(
classify_mesh_readiness_failure(true),
MeshReadinessFailure::RoutingNeverCompleted
);
}
#[test]
fn readiness_messages_are_distinct_and_actionable() {
let catalog = mesh_readiness_failure_message(
MeshReadinessFailure::CatalogNeverSynced,
"auto",
"HTTP 429",
);
let routing = mesh_readiness_failure_message(
MeshReadinessFailure::RoutingNeverCompleted,
"auto",
"HTTP 503",
);
// Distinct diagnoses, each names the model and carries the raw detail.
assert_ne!(catalog, routing);
assert!(catalog.contains("network path"));
assert!(catalog.contains("HTTP 429"));
assert!(routing.contains("did not complete"));
assert!(routing.contains("HTTP 503"));
}
#[test]
fn mesh_status_cursor_uses_relay_composite_tiebreak() {
let event = nostr::EventBuilder::new(nostr::Kind::TextNote, "status")
.custom_created_at(nostr::Timestamp::from(1_234))
.sign_with_keys(&nostr::Keys::generate())
.expect("sign test status");
let mut filter = mesh_llm::mesh_status_filter();
let cursor = advance_mesh_status_cursor(&mut filter, std::slice::from_ref(&event))
.expect("advance status cursor");
assert_eq!(cursor, (1_234, event.id.to_hex()));
assert_eq!(filter["until"], serde_json::json!(1_234));
assert_eq!(filter["before_id"], serde_json::json!(event.id.to_hex()));
assert_eq!(
filter["limit"],
serde_json::json!(mesh_llm::MESH_STATUS_PAGE_SIZE)
);
}
#[test]
fn pick_serve_target_returns_first_match_for_model() {
let targets = vec![
target("model-a", "addr-a"),
target("model-b", "addr-b1"),
target("model-b", "addr-b2"),
];
// Matches by model id and returns the first such target.
assert_eq!(
pick_serve_target_for_model(targets, "model-b").map(|t| t.endpoint_addr),
Some("addr-b1".to_string())
);
}
#[test]
fn pick_serve_target_normalizes_main_revision() {
let targets = vec![target("org/model@main:q4", "addr")];
assert_eq!(
pick_serve_target_for_model(targets, "org/model:q4").map(|target| target.endpoint_addr),
Some("addr".to_string())
);
}
#[test]
fn pick_serve_target_auto_takes_any_live_target() {
let targets = vec![target("model-a", "addr-a"), target("model-b", "addr-b")];
// "auto" delegates model choice to the mesh router; any live target
// is a valid bootstrap peer (first one wins).
assert_eq!(
pick_serve_target_for_model(targets, crate::mesh_llm::AUTO_MODEL_ID)
.map(|t| t.endpoint_addr),
Some("addr-a".to_string())
);
// But auto with zero live targets still falls closed.
assert_eq!(
pick_serve_target_for_model(Vec::new(), crate::mesh_llm::AUTO_MODEL_ID),
None
);
}
#[test]
fn pick_serve_target_none_when_model_not_hosted() {
let targets = vec![target("model-a", "addr-a")];
// No live target serves this model -> caller falls closed.
assert_eq!(pick_serve_target_for_model(targets, "model-missing"), None);
}
#[test]
fn share_stop_tears_down_serve_but_not_client() {
// Stopping "Share compute" tears down a serve node (we were sharing)
// but must leave a client node alone (we are consuming a peer). This is
// the backend half of the toggle-on regression: a client node occupies
// the single slot and reports state:"running", and the stop path must
// not kill it.
assert!(
share_stop_should_teardown(mesh_llm::MeshNodeMode::Serve),
"serve node is our sharing runtime; stop must tear it down"
);
assert!(
!share_stop_should_teardown(mesh_llm::MeshNodeMode::Client),
"client node is a consume session; stop must NOT tear it down"
);
}
#[test]
fn client_status_serializes_with_running_state_and_client_mode() {
// Contract pin for the TS mock (e2eBridge.ts) and the frontend
// predicate: a consuming node serializes as
// {"state":"running","mode":"client"}. If serde renaming drifts, the
// hand-written mock shape and `deriveMeshShareToggle` would silently
// stop matching the real IPC payload.
let status = mesh_llm::MeshNodeStatus {
state: mesh_llm::MeshNodeState::Running,
mode: Some(mesh_llm::MeshNodeMode::Client),
// `MeshHealth::ok()` is module-private; build via the public fields.
health: mesh_llm::MeshHealth {
status: mesh_llm::MeshHealthStatus::Ok,
reason: None,
},
api_base_url: Some("http://127.0.0.1:9337/v1".to_string()),
console_url: None,
model_id: None,
model_name: None,
invite_token: None,
endpoint_id: None,
device_id: None,
device_name: None,
};
let value = serde_json::to_value(&status).expect("serialize mesh status");
assert_eq!(value["state"], serde_json::json!("running"));
assert_eq!(value["mode"], serde_json::json!("client"));
}
#[tokio::test]
async fn cold_client_preflight_requires_explicit_target() {
let state = build_app_state();
let error = ensure_client_node_for_model(&state, "demo/model", None)
.await
.expect_err("cold relay-mesh preflight must not auto-pick a target");
assert_eq!(error, RELAY_MESH_RUNTIME_NO_TARGET);
}
/// Acceptance-critical regression for dropping the serve-vs-client guard.
///
/// Before this change, `ensure_client_node_for_model` hard-errored whenever
/// the running runtime was in `Serve` mode ("stop sharing before using
/// Buzz shared compute as a client"). That forbade exactly what a user should be
/// able to do: host model A while pointing an agent at a different model B
/// through the same `9337` ingress.
///
/// This test starts a real serve runtime and asserts that a follow-up
/// preflight for a *different* model and no explicit target still reuses the
/// existing runtime. Cold starts without a target are rejected before mesh-llm
/// startup; running runtimes are already joined to whatever target the
/// frontend selected earlier.
///
/// Hardware-gated (`#[ignore]`): loads a real model. Run with:
/// cargo test -p buzz-desktop --features mesh-llm \
/// ensure_serve_runtime_serves_other_model -- --ignored --nocapture
#[test]
#[ignore = "loads a real model; run manually with --ignored"]
fn ensure_serve_runtime_serves_other_model() {
std::thread::Builder::new()
.name("mesh-hardware-acceptance".to_string())
.stack_size(mesh_llm::MESH_WORKER_STACK_SIZE)
.spawn(|| {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_stack_size(mesh_llm::MESH_WORKER_STACK_SIZE)
.enable_all()
.build()
.expect("build mesh acceptance runtime");
runtime.block_on(async {
const HOSTED_MODEL: &str = "jc-builds/SmolLM2-135M-Instruct-Q4_K_M-GGUF:Q4_K_M";
const OTHER_MODEL: &str = "some/other-model-not-hosted-locally:Q4_K_M";
let state = build_app_state();
// Start a serve runtime hosting HOSTED_MODEL — this is the "Share
// compute" path.
let serve =
mesh_llm::DesktopMeshRuntime::start(mesh_llm::StartMeshNodeRequest {
mode: mesh_llm::MeshNodeMode::Serve,
model_id: Some(HOSTED_MODEL.to_string()),
max_vram_gb: None,
join_token: None,
trusted_owner_ids: None,
})
.await
.expect("serve runtime should start");
let serve_status = serve.status().await.expect("serve status");
let serve_base = serve_status.api_base_url.clone();
assert_eq!(serve_status.mode, Some(mesh_llm::MeshNodeMode::Serve));
{
let mut runtime = state.mesh_llm_runtime.lock().await;
*runtime = Some(serve);
}
// Preflight for a DIFFERENT model with no explicit target. Old code:
// Err(...sharing compute...). New code: reuse the running ingress.
let status = ensure_client_node_for_model(&state, OTHER_MODEL, None)
.await
.expect("serve runtime must not reject a different-model preflight");
// It returns the SAME running node — agents keep using A's 9337, and
// the router decides routability for OTHER_MODEL per request.
assert_eq!(
status.mode,
Some(mesh_llm::MeshNodeMode::Serve),
"preflight should reuse the existing serve runtime, not spin up a client"
);
assert_eq!(
status.api_base_url, serve_base,
"agent must be pointed at the existing serve node's ingress"
);
// Clean up the runtime.
let taken = state.mesh_llm_runtime.lock().await.take();
if let Some(runtime) = taken {
let _ = runtime.stop().await;
}
});
})
.expect("spawn mesh acceptance thread")
.join()
.expect("mesh acceptance thread panicked");
}
}
#[path = "mesh_llm_tests.rs"]
mod tests;

View File

@@ -0,0 +1,499 @@
use super::*;
use crate::app_state::build_app_state;
fn target(model_id: &str, endpoint_addr: &str) -> mesh_llm::MeshServeTarget {
mesh_llm::MeshServeTarget {
model_id: model_id.to_string(),
model_name: None,
endpoint_addr: endpoint_addr.to_string(),
reporter_pubkey: None,
owner_id: None,
node_name: None,
capacity: None,
endpoint_id: None,
device_id: None,
device_name: None,
}
}
fn reported_target(
reporter_pubkey: &str,
model_id: &str,
endpoint_addr: &str,
) -> mesh_llm::MeshServeTarget {
let mut target = target(model_id, endpoint_addr);
target.reporter_pubkey = Some(reporter_pubkey.to_string());
target.owner_id = Some(reporter_pubkey.to_string());
target
}
#[test]
fn buzz_mesh_join_uses_the_same_live_member_from_every_other_node() {
let targets = vec![
reported_target("member-c", "model-c", "addr-c"),
reported_target("member-a", "model-a", "addr-a"),
reported_target("member-b", "model-b", "addr-b"),
];
assert_eq!(
buzz_mesh_join_targets(targets.clone(), "member-b")
.into_iter()
.next()
.map(|target| target.endpoint_addr),
Some("addr-a".to_string())
);
assert_eq!(
buzz_mesh_join_targets(targets, "member-c")
.into_iter()
.next()
.map(|target| target.endpoint_addr),
Some("addr-a".to_string())
);
}
#[test]
fn buzz_mesh_bootstrap_member_does_not_dial_itself() {
let targets = vec![
reported_target("member-b", "model-b", "addr-b"),
reported_target("member-a", "model-a", "addr-a"),
];
assert_eq!(
buzz_mesh_join_targets(targets, "MEMBER-A")
.into_iter()
.next(),
Some(reported_target("member-b", "model-b", "addr-b"))
);
}
#[test]
fn buzz_mesh_join_ignores_targets_without_a_validated_reporter() {
let targets = vec![
target("unbound-model", "unbound-addr"),
reported_target("member-b", "model-b", "addr-b"),
];
assert_eq!(
buzz_mesh_join_targets(targets, "member-c")
.into_iter()
.next()
.map(|target| target.endpoint_addr),
Some("addr-b".to_string())
);
}
#[test]
fn buzz_mesh_join_keeps_other_device_with_the_same_member_key() {
let mut self_target = reported_target("same-member", "model-a", "self-addr");
self_target.owner_id = Some("owner-self".to_string());
let mut other_device = reported_target("same-member", "model-b", "other-addr");
other_device.owner_id = Some("owner-other".to_string());
assert_eq!(
buzz_mesh_join_targets(vec![self_target, other_device], "owner-self")
.into_iter()
.next()
.map(|target| target.endpoint_addr),
Some("other-addr".to_string())
);
}
#[test]
fn buzz_mesh_name_is_stable_and_does_not_expose_the_relay() {
let first = buzz_mesh_name_for_relay("WSS://EXAMPLE.COM/");
let second = buzz_mesh_name_for_relay("wss://example.com:443/some/path?ignored=yes");
let other_relay = buzz_mesh_name_for_relay("wss://other.example.com");
assert_eq!(first, second);
assert_ne!(first, other_relay);
assert!(first.starts_with("buzz-community-"));
assert!(!first.contains("example"));
}
#[test]
fn readiness_failure_is_catalog_sync_when_model_never_visible() {
assert_eq!(
classify_mesh_readiness_failure(false),
MeshReadinessFailure::CatalogNeverSynced
);
}
#[test]
fn readiness_failure_is_routing_when_model_was_visible() {
assert_eq!(
classify_mesh_readiness_failure(true),
MeshReadinessFailure::RoutingNeverCompleted
);
}
#[test]
fn readiness_messages_are_distinct_and_actionable() {
let catalog = mesh_readiness_failure_message(
MeshReadinessFailure::CatalogNeverSynced,
"auto",
"HTTP 429",
);
let routing = mesh_readiness_failure_message(
MeshReadinessFailure::RoutingNeverCompleted,
"auto",
"HTTP 503",
);
// Distinct diagnoses, each names the model and carries the raw detail.
assert_ne!(catalog, routing);
assert!(catalog.contains("network path"));
assert!(catalog.contains("HTTP 429"));
assert!(routing.contains("did not complete"));
assert!(routing.contains("HTTP 503"));
}
#[test]
fn mesh_status_cursor_uses_relay_composite_tiebreak() {
let event = nostr::EventBuilder::new(nostr::Kind::TextNote, "status")
.custom_created_at(nostr::Timestamp::from(1_234))
.sign_with_keys(&nostr::Keys::generate())
.expect("sign test status");
let mut filter = mesh_llm::mesh_status_filter();
let cursor = advance_mesh_status_cursor(&mut filter, std::slice::from_ref(&event))
.expect("advance status cursor");
assert_eq!(cursor, (1_234, event.id.to_hex()));
assert_eq!(filter["until"], serde_json::json!(1_234));
assert_eq!(filter["before_id"], serde_json::json!(event.id.to_hex()));
assert_eq!(
filter["limit"],
serde_json::json!(mesh_llm::MESH_STATUS_PAGE_SIZE)
);
}
#[test]
fn pick_serve_target_returns_first_match_for_model() {
let targets = vec![
target("model-a", "addr-a"),
target("model-b", "addr-b1"),
target("model-b", "addr-b2"),
];
// Matches by model id and returns the first such target.
assert_eq!(
pick_serve_target_for_model(targets, "model-b").map(|t| t.endpoint_addr),
Some("addr-b1".to_string())
);
}
#[test]
fn pick_serve_target_normalizes_main_revision() {
let targets = vec![target("org/model@main:q4", "addr")];
assert_eq!(
pick_serve_target_for_model(targets, "org/model:q4").map(|target| target.endpoint_addr),
Some("addr".to_string())
);
}
#[test]
fn pick_serve_target_auto_takes_any_live_target() {
let targets = vec![target("model-a", "addr-a"), target("model-b", "addr-b")];
// "auto" delegates model choice to the mesh router; any live target
// is a valid bootstrap peer (first one wins).
assert_eq!(
pick_serve_target_for_model(targets, crate::mesh_llm::AUTO_MODEL_ID)
.map(|t| t.endpoint_addr),
Some("addr-a".to_string())
);
// But auto with zero live targets still falls closed.
assert_eq!(
pick_serve_target_for_model(Vec::new(), crate::mesh_llm::AUTO_MODEL_ID),
None
);
}
#[test]
fn pick_serve_target_none_when_model_not_hosted() {
let targets = vec![target("model-a", "addr-a")];
// No live target serves this model -> caller falls closed.
assert_eq!(pick_serve_target_for_model(targets, "model-missing"), None);
}
#[test]
fn share_stop_tears_down_serve_but_not_client() {
// Stopping "Share compute" tears down a serve node (we were sharing)
// but must leave a client node alone (we are consuming a peer). This is
// the backend half of the toggle-on regression: a client node occupies
// the single slot and reports state:"running", and the stop path must
// not kill it.
assert!(
share_stop_should_teardown(mesh_llm::MeshNodeMode::Serve),
"serve node is our sharing runtime; stop must tear it down"
);
assert!(
!share_stop_should_teardown(mesh_llm::MeshNodeMode::Client),
"client node is a consume session; stop must NOT tear it down"
);
}
#[test]
fn share_start_restarts_to_replace_only_client_runtimes() {
assert_eq!(
mesh_start_plan(mesh_llm::MeshNodeMode::Serve, None),
MeshStartPlan::Start
);
assert_eq!(
mesh_start_plan(
mesh_llm::MeshNodeMode::Serve,
Some(mesh_llm::MeshNodeMode::Client),
),
MeshStartPlan::RestartToReplaceClient
);
assert_eq!(
mesh_start_plan(
mesh_llm::MeshNodeMode::Serve,
Some(mesh_llm::MeshNodeMode::Serve),
),
MeshStartPlan::RejectOccupied
);
assert_eq!(
mesh_start_plan(
mesh_llm::MeshNodeMode::Client,
Some(mesh_llm::MeshNodeMode::Client),
),
MeshStartPlan::RejectOccupied
);
}
#[test]
fn client_status_serializes_with_running_state_and_client_mode() {
// Contract pin for the TS mock (e2eBridge.ts) and the frontend
// predicate: a consuming node serializes as
// {"state":"running","mode":"client"}. If serde renaming drifts, the
// hand-written mock shape and `deriveMeshShareToggle` would silently
// stop matching the real IPC payload.
let status = mesh_llm::MeshNodeStatus {
state: mesh_llm::MeshNodeState::Running,
mode: Some(mesh_llm::MeshNodeMode::Client),
// `MeshHealth::ok()` is module-private; build via the public fields.
health: mesh_llm::MeshHealth {
status: mesh_llm::MeshHealthStatus::Ok,
reason: None,
},
api_base_url: Some("http://127.0.0.1:9337/v1".to_string()),
console_url: None,
model_id: None,
model_name: None,
invite_token: None,
endpoint_id: None,
device_id: None,
device_name: None,
};
let value = serde_json::to_value(&status).expect("serialize mesh status");
assert_eq!(value["state"], serde_json::json!("running"));
assert_eq!(value["mode"], serde_json::json!("client"));
}
#[tokio::test]
async fn cold_client_preflight_requires_explicit_target() {
let state = build_app_state();
let error = ensure_client_node_for_model(&state, "demo/model", None)
.await
.expect_err("cold relay-mesh preflight must not auto-pick a target");
assert_eq!(error, RELAY_MESH_RUNTIME_NO_TARGET);
}
/// Acceptance-critical regression for dropping the serve-vs-client guard.
///
/// Before this change, `ensure_client_node_for_model` hard-errored whenever
/// the running runtime was in `Serve` mode ("stop sharing before using
/// Buzz shared compute as a client"). That forbade exactly what a user should be
/// able to do: host model A while pointing an agent at a different model B
/// through the same `9337` ingress.
///
/// This test starts a real serve runtime and asserts that a follow-up
/// preflight for a *different* model and no explicit target still reuses the
/// existing runtime. Cold starts without a target are rejected before mesh-llm
/// startup; running runtimes are already joined to whatever target the
/// frontend selected earlier.
///
/// Hardware-gated (`#[ignore]`): loads a real model. Run with:
/// cargo test -p buzz-desktop --features mesh-llm \
/// ensure_serve_runtime_serves_other_model -- --ignored --nocapture
#[test]
#[ignore = "loads a real model; run manually with --ignored"]
fn ensure_serve_runtime_serves_other_model() {
std::thread::Builder::new()
.name("mesh-hardware-acceptance".to_string())
.stack_size(mesh_llm::MESH_WORKER_STACK_SIZE)
.spawn(|| {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_stack_size(mesh_llm::MESH_WORKER_STACK_SIZE)
.enable_all()
.build()
.expect("build mesh acceptance runtime");
runtime.block_on(async {
const DEFAULT_HOSTED_MODEL: &str =
"jc-builds/SmolLM2-135M-Instruct-Q4_K_M-GGUF:Q4_K_M";
const OTHER_MODEL: &str = "some/other-model-not-hosted-locally:Q4_K_M";
let hosted_model = std::env::var("BUZZ_MESH_TEST_MODEL")
.unwrap_or_else(|_| DEFAULT_HOSTED_MODEL.to_string());
let state = build_app_state();
// Start a serve runtime hosting HOSTED_MODEL — this is the "Share
// compute" path.
let serve =
mesh_llm::DesktopMeshRuntime::start(mesh_llm::StartMeshNodeRequest {
mode: mesh_llm::MeshNodeMode::Serve,
model_id: Some(hosted_model.clone()),
max_vram_gb: None,
join_token: None,
mesh_name: None,
trusted_owner_ids: None,
})
.await
.expect("serve runtime should start");
let serve_status = serve.status().await.expect("serve status");
let serve_base = serve_status
.api_base_url
.clone()
.expect("serve runtime must expose its local API base");
assert_eq!(serve_status.mode, Some(mesh_llm::MeshNodeMode::Serve));
{
let mut runtime = state.mesh_llm_runtime.lock().await;
*runtime = Some(serve);
}
// Concurrent GUI agent starts all reuse this one machine-scoped
// runtime. None may create a per-agent client/serve node.
let preflights = tokio::join!(
ensure_client_node_for_model(&state, OTHER_MODEL, None),
ensure_client_node_for_model(&state, OTHER_MODEL, None),
ensure_client_node_for_model(&state, OTHER_MODEL, None),
ensure_client_node_for_model(&state, OTHER_MODEL, None),
);
let statuses = [
preflights.0,
preflights.1,
preflights.2,
preflights.3,
]
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("all agent preflights must reuse the serve runtime");
// It returns the SAME running node — agents keep using A's 9337, and
// the router decides routability for OTHER_MODEL per request.
for status in statuses {
assert_eq!(
status.mode,
Some(mesh_llm::MeshNodeMode::Serve),
"preflight should reuse the existing serve runtime, not spin up a client"
);
assert_eq!(
status.api_base_url.as_deref(),
Some(serve_base.as_str()),
"every agent must be pointed at the machine's existing ingress"
);
}
// A standalone Share Compute runtime must advertise exactly
// one physical model and serve inference through `auto`.
let http = reqwest::Client::new();
let catalog_deadline =
tokio::time::Instant::now() + std::time::Duration::from_secs(120);
let catalog = loop {
let body = http
.get(format!("{serve_base}/models"))
.send()
.await
.expect("query single-node catalog")
.error_for_status()
.expect("single-node catalog status")
.json::<serde_json::Value>()
.await
.expect("parse single-node catalog");
let physical_count = body["data"]
.as_array()
.map(|models| {
models
.iter()
.filter_map(|model| model["id"].as_str())
.filter(|model| *model != "mesh")
.count()
})
.unwrap_or_default();
if physical_count > 0 {
break body;
}
assert!(
tokio::time::Instant::now() < catalog_deadline,
"single Share Compute model never became ready: {body}"
);
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
};
let physical_models = catalog["data"]
.as_array()
.expect("catalog data")
.iter()
.filter_map(|model| model["id"].as_str())
.filter(|model| *model != "mesh")
.collect::<Vec<_>>();
assert_eq!(
physical_models.len(),
1,
"single Share Compute node must advertise one physical model: {catalog}"
);
assert!(
physical_models[0].contains(hosted_model.split(':').next().unwrap_or("")),
"catalog must contain the hosted model: {catalog}"
);
let inference_deadline =
tokio::time::Instant::now() + std::time::Duration::from_secs(120);
let inference = loop {
let response = http
.post(format!("{serve_base}/chat/completions"))
.json(&serde_json::json!({
"model": "auto",
"messages": [{
"role": "user",
"content": "Reply with exactly BUZZ_SINGLE_SHARE_OK and nothing else."
}],
"max_tokens": 512,
"temperature": 0
}))
.send()
.await
.expect("single-node auto inference");
if response.status().is_success() {
break response
.json::<serde_json::Value>()
.await
.expect("parse single-node inference");
}
let status = response.status();
let body = response.text().await.unwrap_or_default();
assert!(
tokio::time::Instant::now() < inference_deadline,
"single-node auto inference never became ready: HTTP {status}: {body}"
);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
};
let answer = inference["choices"][0]["message"]["content"]
.as_str()
.unwrap_or_default();
assert!(
!answer.trim().is_empty(),
"single-node auto must produce visible output: {inference}"
);
// Clean up the runtime.
let taken = state.mesh_llm_runtime.lock().await.take();
if let Some(runtime) = taken {
let _ = runtime.stop().await;
}
});
})
.expect("spawn mesh acceptance thread")
.join()
.expect("mesh acceptance thread panicked");
}

View File

@@ -2,6 +2,8 @@ pub const RELAY_MESH_API_BASE_URL: &str = "http://127.0.0.1:9337/v1";
pub const RELAY_MESH_API_KEY_PLACEHOLDER: &str = "buzz-mesh-local";
pub const RELAY_MESH_PROVIDER_ID: &str = "relay-mesh";
pub const RELAY_MESH_AUTO_MODEL_ID: &str = "auto";
#[cfg(feature = "mesh-llm")]
pub const RELAY_MESH_PREFER_MESH_FOR_AUTO_ENV: &str = "BUZZ_AGENT_PREFER_MESH_FOR_AUTO";
/// Translate the native Buzz shared compute provider into the OpenAI-compatible
/// transport understood by buzz-agent. These are derived runtime details, not
@@ -32,6 +34,14 @@ pub fn apply_relay_mesh_env(
RELAY_MESH_API_KEY_PLACEHOLDER.to_string(),
);
env.insert("OPENAI_COMPAT_API".to_string(), "chat".to_string());
// Buzz owns the meaning of relay-mesh `auto`: buzz-agent dynamically uses
// mesh-llm's virtual Mixture-of-Agents model whenever the live catalog says
// at least two distinct models are available, and otherwise keeps the
// router's normal single-model `auto` behavior.
env.insert(
RELAY_MESH_PREFER_MESH_FOR_AUTO_ENV.to_string(),
"1".to_string(),
);
// Keep the requested response inside smaller local-model context windows,
// and spend that budget on an answer/tool call instead of hidden reasoning.
// Without both settings Qwen3 either fails the router's fit check at the
@@ -66,5 +76,10 @@ mod tests {
env.get("BUZZ_AGENT_THINKING_EFFORT").map(String::as_str),
Some("none")
);
assert_eq!(
env.get(RELAY_MESH_PREFER_MESH_FOR_AUTO_ENV)
.map(String::as_str),
Some("1")
);
}
}

View File

@@ -27,11 +27,17 @@ const STATUS_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10);
/// not hammer discovery every tick, but a recovered peer is noticed quickly.
const INGRESS_WATCHDOG_BASE: Duration = Duration::from_secs(15);
const INGRESS_WATCHDOG_MAX: Duration = Duration::from_secs(120);
/// A Share Compute node may start before another member's signed status reaches
/// the relay. Recheck promptly so simultaneous starts converge into one Buzz
/// mesh instead of remaining independent islands.
const MESH_JOIN_POLL_INTERVAL: Duration = Duration::from_secs(15);
const MESH_JOIN_RETRY_MAX: Duration = Duration::from_secs(120);
pub struct MeshCoordinator {
_status_publisher: tokio::task::JoinHandle<()>,
_roster_watcher: tokio::task::JoinHandle<()>,
_ingress_watchdog: tokio::task::JoinHandle<()>,
_mesh_join_watcher: tokio::task::JoinHandle<()>,
}
/// Start the runtime-owned status publisher and admission-roster watcher.
@@ -62,12 +68,25 @@ pub async fn start_coordinator(app: AppHandle) {
let mut pending_shrink: Option<Vec<String>> = None;
loop {
tokio::time::sleep(ROSTER_POLL_INTERVAL).await;
let state = roster_app.state::<AppState>();
if let Err(error) = reconcile_roster(&state, &mut pending_shrink).await {
if let Err(error) = reconcile_roster(&roster_app, &mut pending_shrink).await {
eprintln!("buzz-mesh: roster reconcile failed: {error}");
}
}
});
let join_app = app.clone();
let mesh_join_watcher = tokio::spawn(async move {
let mut sleep_for = MESH_JOIN_POLL_INTERVAL;
loop {
tokio::time::sleep(sleep_for).await;
match reconcile_buzz_mesh_join(&join_app).await {
Ok(()) => sleep_for = MESH_JOIN_POLL_INTERVAL,
Err(error) => {
eprintln!("buzz-mesh: community mesh join reconcile failed: {error}");
sleep_for = (sleep_for * 2).min(MESH_JOIN_RETRY_MAX);
}
}
}
});
// Brad #2304 / #2062: ensure_relay_mesh_for_record only runs on explicit
// start + launch restore. After launch, local buzz-agent processes talk
@@ -98,14 +117,85 @@ pub async fn start_coordinator(app: AppHandle) {
_status_publisher: status_publisher,
_roster_watcher: roster_watcher,
_ingress_watchdog: ingress_watchdog,
_mesh_join_watcher: mesh_join_watcher,
});
} else {
status_publisher.abort();
roster_watcher.abort();
ingress_watchdog.abort();
mesh_join_watcher.abort();
}
}
/// Join an isolated runtime to the existing Buzz community mesh. The relay is
/// discovery only: the selected endpoint is member-signed and validated, then
/// MeshLLM establishes the encrypted peer transport itself.
async fn reconcile_buzz_mesh_join(app: &AppHandle) -> Result<(), String> {
let state = app.state::<AppState>();
let peer_ids = {
let runtime = state.mesh_llm_runtime.lock().await;
let Some(runtime) = runtime.as_ref() else {
return Ok(());
};
let payload = runtime
.status_report_payload()
.await
.map_err(|error| error.to_string())?;
visible_peer_ids(&payload)
};
let targets = crate::commands::mesh_llm::resolve_buzz_mesh_join_targets(&state).await?;
let Some(target) = targets
.into_iter()
.find(|target| !target_is_visible(target, &peer_ids))
else {
return Ok(());
};
let runtime = state.mesh_llm_runtime.lock().await;
let Some(runtime) = runtime.as_ref() else {
return Ok(());
};
let payload = runtime
.status_report_payload()
.await
.map_err(|error| error.to_string())?;
if target_is_visible(&target, &visible_peer_ids(&payload)) {
return Ok(());
}
runtime
.dial_endpoint_addr(target.endpoint_addr)
.await
.map_err(|error| format!("mesh join failed: {error:#}"))
}
fn visible_peer_ids(payload: &serde_json::Value) -> Vec<String> {
payload
.get("peers")
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter_map(|peer| peer.get("id").and_then(serde_json::Value::as_str))
.map(|id| id.trim().to_ascii_lowercase())
.filter(|id| !id.is_empty())
.collect()
}
fn target_is_visible(target: &crate::mesh_llm::MeshServeTarget, peer_ids: &[String]) -> bool {
let Some(endpoint_id) = target.endpoint_id.as_deref() else {
return false;
};
let endpoint_id = endpoint_id.trim().to_ascii_lowercase();
if endpoint_id.is_empty() {
return false;
}
peer_ids.iter().any(|peer_id| {
let peer_id = peer_id.trim().to_ascii_lowercase();
!peer_id.is_empty()
&& (endpoint_id.starts_with(&peer_id) || peer_id.starts_with(&endpoint_id))
})
}
/// Outcome of a roster reconcile decision.
#[derive(Debug, PartialEq, Eq)]
enum RosterReconcileAction {
@@ -170,9 +260,10 @@ fn roster_reconcile_action(
}
async fn reconcile_roster(
state: &AppState,
app: &AppHandle,
pending_shrink: &mut Option<Vec<String>>,
) -> Result<(), String> {
let state = app.state::<AppState>();
let current_request = {
let runtime = state.mesh_llm_runtime.lock().await;
match runtime.as_ref() {
@@ -192,7 +283,7 @@ async fn reconcile_roster(
// other member on a transient relay blip (the flapping restart loop). Keep
// the current allowlist and try again on the next poll. A shrink is held
// for one extra poll (hysteresis) so a single short-read never tears down.
let query = crate::commands::mesh_llm::resolve_trusted_owner_ids(state).await;
let query = crate::commands::mesh_llm::resolve_trusted_owner_ids(&state).await;
let fresh = match roster_reconcile_action(current_owners, pending_shrink.as_deref(), query) {
RosterReconcileAction::Keep => {
*pending_shrink = None;
@@ -209,8 +300,27 @@ async fn reconcile_roster(
}
};
let mut request = current_request;
let mut request = current_request.clone();
request.trusted_owner_ids = Some(fresh);
// Bootstrap endpoints are live device state, not configuration. The
// endpoint used at the previous start may belong to the member that just
// left or to a device whose iroh identity rotated while offline. Resolve a
// fresh validated peer for this restart; starting isolated is safe because
// the join watcher will converge it when a member next publishes.
request.join_token = match crate::commands::mesh_llm::resolve_buzz_mesh_join_targets(&state)
.await
{
Ok(targets) => targets
.into_iter()
.next()
.map(|target| target.endpoint_addr),
Err(error) => {
eprintln!(
"buzz-mesh: could not refresh bootstrap endpoint for roster restart; starting isolated: {error}"
);
None
}
};
let mut guard = state.mesh_llm_runtime.lock().await;
let startup_pending = match guard.as_ref() {
Some(runtime) => runtime.is_starting().await,
@@ -222,12 +332,29 @@ async fn reconcile_roster(
);
return Ok(());
}
if guard
.as_ref()
.is_some_and(|runtime| runtime.start_request() != &current_request)
{
// The runtime changed while relay discovery was in flight. Its own
// request is now authoritative; the next poll will reconcile that
// runtime instead of tearing down a fresh replacement from this stale
// snapshot.
return Ok(());
}
let Some(running) = guard.take() else {
return Ok(());
};
eprintln!("buzz-mesh: membership roster changed; restarting mesh node with fresh allowlist");
if let Err(error) = running.stop().await {
eprintln!("buzz-mesh: stopping mesh node for roster restart failed: {error}");
drop(guard);
eprintln!(
"buzz-mesh: stopping mesh node for roster restart failed; restarting Buzz instead of racing the occupied ingress: {error}"
);
app.request_restart();
return Err(format!(
"mesh node shutdown failed during roster change: {error}"
));
}
let replacement = crate::mesh_llm::DesktopMeshRuntime::start(request)
.await
@@ -357,6 +484,51 @@ mod tests {
use super::*;
fn join_target(endpoint_id: Option<&str>) -> crate::mesh_llm::MeshServeTarget {
crate::mesh_llm::MeshServeTarget {
model_id: "model".to_string(),
model_name: None,
endpoint_addr: "iroh://example".to_string(),
reporter_pubkey: Some("member".to_string()),
owner_id: Some("owner".to_string()),
node_name: None,
capacity: None,
endpoint_id: endpoint_id.map(str::to_string),
device_id: None,
device_name: None,
}
}
#[test]
fn visible_peer_ids_reads_the_sdk_status_shape() {
assert_eq!(
visible_peer_ids(&serde_json::json!({
"peers": [{"id": " ABC123 "}, {"id": "def456"}, {"name": "ignored"}]
})),
vec!["abc123".to_string(), "def456".to_string()]
);
}
#[test]
fn target_visibility_accepts_sdk_short_endpoint_ids() {
let target = join_target(Some("ABC1234567890"));
assert!(target_is_visible(&target, &["abc123".to_string()]));
assert!(!target_is_visible(&target, &["def456".to_string()]));
}
#[test]
fn target_without_an_endpoint_id_is_never_treated_as_connected() {
assert!(!target_is_visible(
&join_target(None),
&["abc123".to_string()]
));
assert!(!target_is_visible(
&join_target(Some(" ")),
&["abc123".to_string()]
));
}
// Regression: a transient roster-query failure must never restart the node
// down to self-only. Before the fix, `resolve_trusted_owner_ids` returned
// an empty Vec on error, which `reconcile_roster` read as "roster changed

View File

@@ -5,9 +5,10 @@ use sha2::{Digest, Sha256};
use super::{dedupe_models, MeshAvailability, MeshModelOption, MeshServeTarget, MESH_STATUS_KIND};
/// Running-node status notes are refreshed every 45 seconds. Ignore notes older
/// than two minutes so crashed/offline devices stop contributing compute or
/// admission identities without requiring a relay-side cleanup job.
/// Running-node status notes are refreshed every 45 seconds. Routing ignores
/// notes older than two minutes so crashed/offline devices stop contributing
/// compute without a relay-side cleanup job. Admission intentionally does not:
/// Buzz membership, rather than device liveness, is the trust boundary.
pub(super) const STATUS_FRESHNESS_SECS: u64 = 120;
pub(crate) const MESH_STATUS_PAGE_SIZE: usize = 100;
@@ -199,13 +200,14 @@ pub fn availability_from_events(events: Vec<nostr::Event>) -> MeshAvailability {
let Ok(content) = serde_json::from_str::<serde_json::Value>(&event.content) else {
continue;
};
if owner_id_from_status_event(&event).is_none() {
let Some(owner_id) = owner_id_from_status_event(&event) else {
continue;
}
};
if !endpoint_binding_is_valid(&event, &content) {
continue;
}
saw_valid_status = true;
let reporter_pubkey = event.pubkey.to_hex().to_ascii_lowercase();
let mut serve_targets = content
.get("serveTargets")
.or_else(|| content.get("serve_targets"))
@@ -218,6 +220,8 @@ pub fn availability_from_events(events: Vec<nostr::Event>) -> MeshAvailability {
super::transport_policy::validate_advertised_endpoint(&target.endpoint_addr)
.ok()?;
target.endpoint_addr = validated.join_token;
target.reporter_pubkey = Some(reporter_pubkey.clone());
target.owner_id = Some(owner_id.clone());
if target.endpoint_id.is_none() {
target.endpoint_id = Some(validated.endpoint_id);
}

View File

@@ -29,6 +29,9 @@ pub(crate) use recovery::{
MeshRuntimeRecovery,
};
mod usage;
pub use usage::{serving_usage_from_payload, MeshServingUsage};
mod transport_policy;
#[cfg(test)]
use transport_policy::iroh_relay_mode_from;
@@ -84,6 +87,15 @@ pub struct MeshServeTarget {
pub model_id: String,
pub model_name: Option<String>,
pub endpoint_addr: String,
/// Buzz member that signed the discovery note containing this target.
/// Populated after signature/membership validation; never trusted from the
/// note payload itself.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reporter_pubkey: Option<String>,
/// Per-runtime MeshLLM owner identity verified by the signed Buzz status.
/// Distinguishes two devices logged into the same Buzz member account.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner_id: Option<String>,
pub node_name: Option<String>,
pub capacity: Option<MeshTargetCapacity>,
#[serde(default)]
@@ -184,6 +196,10 @@ pub struct StartMeshNodeRequest {
pub max_vram_gb: Option<u64>,
#[serde(default)]
pub join_token: Option<String>,
/// Stable, relay-scoped mesh name injected by the Buzz backend. It is not
/// accepted from the frontend and contains no relay address.
#[serde(default, skip_deserializing)]
pub mesh_name: Option<String>,
/// Mesh owner ids admitted to this node (the member roster from
/// member-signed discovery notes). `None` = caller did not resolve a roster
/// (tests, direct invocations): the node runs without allowlist
@@ -214,81 +230,6 @@ pub struct MeshNodeStatus {
pub device_name: Option<String>,
}
/// Host-side "who is using the compute I'm sharing" snapshot.
///
/// Read-only projection of the serving node's own runtime metrics (the same
/// `routing_metrics` / `inflight_requests` the SDK already exposes on the local
/// console). No new trust surface: it reads the node's own status payload.
///
/// The local/remote/endpoint attempt split is what distinguishes *my own*
/// agent (local) from *another member consuming my compute* (remote/endpoint).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct MeshServingUsage {
/// Requests being served right now.
pub inflight: u64,
/// Highest concurrent in-flight seen this session.
pub peak_inflight: u64,
/// Total requests routed through this node.
pub requests_served: u64,
/// Completion tokens produced.
pub tokens_served: u64,
/// Recent decode throughput.
pub tokens_per_second: f64,
/// Requests served for this machine's own agents.
pub local_attempts: u64,
/// Requests served for a remote peer (someone else consuming my compute).
pub remote_attempts: u64,
/// Requests served via an advertised endpoint (also a remote consumer).
pub endpoint_attempts: u64,
/// Other nodes currently visible as peers.
pub peers: u64,
}
impl MeshServingUsage {
/// True when at least one request has been served for a non-local consumer.
#[cfg(test)]
pub fn has_remote_consumers(&self) -> bool {
self.remote_attempts > 0 || self.endpoint_attempts > 0
}
}
/// Pure extractor: project a raw SDK status payload into [`MeshServingUsage`].
///
/// Every field is read defensively (missing → 0) so an SDK shape change
/// degrades to "no usage shown" rather than an error. Kept pure so it can be
/// unit-tested against a captured payload without a live runtime.
pub fn serving_usage_from_payload(payload: &serde_json::Value) -> MeshServingUsage {
let u64_at = |v: &serde_json::Value| v.as_u64().unwrap_or(0);
let rm = payload.get("routing_metrics");
let local = rm.and_then(|m| m.get("local_node"));
let get_u64 = |obj: Option<&serde_json::Value>, key: &str| {
obj.and_then(|o| o.get(key)).map(u64_at).unwrap_or(0)
};
MeshServingUsage {
inflight: local
.and_then(|l| l.get("current_inflight_requests"))
.map(u64_at)
.or_else(|| payload.get("inflight_requests").map(u64_at))
.unwrap_or(0),
peak_inflight: get_u64(local, "peak_inflight_requests"),
requests_served: get_u64(rm, "request_count"),
tokens_served: get_u64(rm, "completion_tokens_observed"),
tokens_per_second: rm
.and_then(|m| m.get("avg_tokens_per_second"))
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0),
local_attempts: get_u64(local, "local_attempt_count"),
remote_attempts: get_u64(local, "remote_attempt_count"),
endpoint_attempts: get_u64(local, "endpoint_attempt_count"),
peers: payload
.get("peers")
.and_then(serde_json::Value::as_array)
.map(|a| a.len() as u64)
.unwrap_or(0),
}
}
pub fn stopped_status() -> MeshNodeStatus {
MeshNodeStatus {
state: MeshNodeState::Off,
@@ -423,6 +364,9 @@ impl DesktopMeshRuntime {
.discovery_mode(MeshDiscoveryMode::Nostr)
.startup_timeout(MESH_STARTUP_TIMEOUT)
.console_ui(true);
if let Some(mesh_name) = request.mesh_name.as_deref() {
builder = builder.mesh_name(mesh_name);
}
builder = match iroh_relay_mode()? {
IrohRelayMode::Disabled => builder.disable_iroh_relays(true),
IrohRelayMode::Default => builder.disable_iroh_relays(false),
@@ -460,6 +404,9 @@ impl DesktopMeshRuntime {
.discovery_mode(MeshDiscoveryMode::Nostr)
.startup_timeout(MESH_CLIENT_MANAGEMENT_TIMEOUT)
.console_ui(true);
if let Some(mesh_name) = request.mesh_name.as_deref() {
builder = builder.mesh_name(mesh_name);
}
builder = match iroh_relay_mode()? {
IrohRelayMode::Disabled => builder.disable_iroh_relays(true),
IrohRelayMode::Default => builder.disable_iroh_relays(false),
@@ -646,6 +593,8 @@ impl DesktopMeshRuntime {
model_id: model.id,
model_name: model.name,
endpoint_addr: endpoint_addr.clone(),
reporter_pubkey: None,
owner_id: None,
node_name: payload
.get("node_id")
.and_then(serde_json::Value::as_str)

View File

@@ -11,6 +11,7 @@ fn pending_client_runtime(
model_id: None,
max_vram_gb: None,
join_token: Some("initial-token".to_string()),
mesh_name: None,
trusted_owner_ids: None,
};
super::DesktopMeshRuntime {
@@ -692,10 +693,7 @@ fn serving_usage_extracts_local_and_remote_attempts() {
assert_eq!(usage.local_attempts, 4);
assert_eq!(usage.remote_attempts, 0);
assert_eq!(usage.endpoint_attempts, 0);
assert!(
!usage.has_remote_consumers(),
"all-local traffic is not a remote consumer"
);
assert_eq!(usage.remote_attempts + usage.endpoint_attempts, 0);
}
#[test]
@@ -715,10 +713,7 @@ fn serving_usage_flags_remote_consumer() {
assert_eq!(usage.remote_attempts, 6);
assert_eq!(usage.endpoint_attempts, 1);
assert_eq!(usage.peers, 2);
assert!(
usage.has_remote_consumers(),
"remote/endpoint attempts mean someone else is using my compute"
);
assert!(usage.remote_attempts + usage.endpoint_attempts > 0);
}
#[test]
@@ -726,5 +721,5 @@ fn serving_usage_defaults_to_zero_on_missing_fields() {
// SDK shape drift must degrade to "no usage" not panic.
let usage = super::serving_usage_from_payload(&json!({}));
assert_eq!(usage, super::MeshServingUsage::default());
assert!(!usage.has_remote_consumers());
assert_eq!(usage.remote_attempts + usage.endpoint_attempts, 0);
}

View File

@@ -243,6 +243,11 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu
let _rearm_guard = state.mesh_recovery.rearm_lock.lock().await;
let recovery = recover_stale_mesh_runtime(&state, MeshRecoveryUrgency::Watchdog).await;
let active_pubkeys = active_managed_agent_pubkeys(&state);
// Mesh participation is resolved through the same definition-authoritative
// path as spawn/restore (#1968): definition → global fallback. A linked
// instance's own bytes never contribute.
let personas = crate::managed_agents::load_personas(app).unwrap_or_default();
let global = crate::managed_agents::load_global_agent_config(app).unwrap_or_default();
match recovery {
MeshRuntimeRecovery::Live
@@ -250,10 +255,9 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu
| MeshRuntimeRecovery::Replaced => return Ok(()),
MeshRuntimeRecovery::RestartRequired => {
let records = crate::managed_agents::load_managed_agents(app).unwrap_or_default();
if !records
.iter()
.any(|record| is_running_relay_mesh_agent(record, &active_pubkeys))
{
if !records.iter().any(|record| {
running_relay_mesh_model_id(record, &active_pubkeys, &personas, &global).is_some()
}) {
// A foreground save may still be bringing up its first ingress.
// Only an already-running consumer justifies an automatic app
// relaunch from the background watchdog.
@@ -272,10 +276,9 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu
}
MeshRuntimeRecovery::Absent => {
let records = crate::managed_agents::load_managed_agents(app).unwrap_or_default();
if !records
.iter()
.any(|record| is_running_relay_mesh_agent(record, &active_pubkeys))
{
if !records.iter().any(|record| {
running_relay_mesh_model_id(record, &active_pubkeys, &personas, &global).is_some()
}) {
return Ok(());
}
}
@@ -285,11 +288,20 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu
let records = crate::managed_agents::load_managed_agents(app).unwrap_or_default();
let mesh_records: Vec<_> = records
.into_iter()
.filter(|record| is_running_relay_mesh_agent(record, &active_pubkeys))
.filter_map(|record| {
running_relay_mesh_model_id(&record, &active_pubkeys, &personas, &global)
.map(|mesh_model_id| (record, mesh_model_id))
})
.collect();
let mut first_error = None;
for record in &mesh_records {
match crate::commands::mesh_llm::ensure_relay_mesh_for_record(app, record, false).await {
for (record, mesh_model_id) in &mesh_records {
match crate::commands::mesh_llm::ensure_relay_mesh_for_record(
app,
Some(mesh_model_id.as_str()),
false,
)
.await
{
Ok(()) => {
if let Err(error) = clear_mesh_last_error_if_set(app, &record.pubkey) {
eprintln!("buzz-mesh: failed to clear recovery error: {error}");
@@ -322,16 +334,28 @@ fn active_managed_agent_pubkeys(state: &AppState) -> HashSet<String> {
.unwrap_or_default()
}
fn is_running_relay_mesh_agent(
/// Effective mesh model for a record that is actively running, or `None`
/// when the record is not a running relay-mesh consumer. Resolution goes
/// through `resolve_effective_relay_mesh_model_id` (definition → global
/// fallback, #1968) so the watchdog agrees with spawn/restore about which
/// agents are mesh-backed.
fn running_relay_mesh_model_id(
record: &crate::managed_agents::ManagedAgentRecord,
active_pubkeys: &HashSet<String>,
) -> bool {
record.backend == crate::managed_agents::BackendKind::Local
&& crate::managed_agents::relay_mesh_model_id(record).is_some()
personas: &[crate::managed_agents::AgentDefinition],
global: &crate::managed_agents::GlobalAgentConfig,
) -> Option<String> {
let running = record.backend == crate::managed_agents::BackendKind::Local
&& active_pubkeys.contains(&record.pubkey.to_ascii_lowercase())
&& record
.runtime_pid
.is_none_or(crate::managed_agents::process_is_running)
.is_none_or(crate::managed_agents::process_is_running);
if !running {
return None;
}
crate::managed_agents::effective_config::resolve_effective_relay_mesh_model_id(
record, personas, global,
)
}
fn persist_mesh_last_error(app: &AppHandle, pubkey: &str, error: &str) -> Result<(), String> {
@@ -458,29 +482,45 @@ mod tests {
#[test]
fn only_running_relay_mesh_agents_trigger_rearm() {
let personas: Vec<crate::managed_agents::AgentDefinition> = Vec::new();
let global = crate::managed_agents::GlobalAgentConfig::default();
let empty = active_set(&[]);
assert!(!is_running_relay_mesh_agent(
assert!(running_relay_mesh_model_id(
&mesh_record("stopped", Some(std::process::id())),
&empty
));
&empty,
&personas,
&global,
)
.is_none());
let active = active_set(&["live"]);
assert!(is_running_relay_mesh_agent(
&mesh_record("live", Some(std::process::id())),
&active
));
assert!(is_running_relay_mesh_agent(
&mesh_record("live", None),
&active
));
assert_eq!(
running_relay_mesh_model_id(
&mesh_record("live", Some(std::process::id())),
&active,
&personas,
&global,
)
.as_deref(),
Some("Qwen3")
);
assert_eq!(
running_relay_mesh_model_id(&mesh_record("live", None), &active, &personas, &global)
.as_deref(),
Some("Qwen3")
);
let mut non_mesh = mesh_record("plain", Some(std::process::id()));
non_mesh.env_vars.clear();
non_mesh.relay_mesh = None;
assert!(!is_running_relay_mesh_agent(
assert!(running_relay_mesh_model_id(
&non_mesh,
&active_set(&["plain"])
));
&active_set(&["plain"]),
&personas,
&global,
)
.is_none());
}
#[test]

View File

@@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
/// Host-side "who is using the compute I'm sharing" snapshot.
///
/// Read-only projection of the serving node's own runtime metrics (the same
/// `routing_metrics` / `inflight_requests` the SDK already exposes on the local
/// console). No new trust surface: it reads the node's own status payload.
///
/// The local/remote/endpoint attempt split is what distinguishes *my own*
/// agent (local) from *another member consuming my compute* (remote/endpoint).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct MeshServingUsage {
/// Requests being served right now.
pub inflight: u64,
/// Highest concurrent in-flight seen this session.
pub peak_inflight: u64,
/// Total requests routed through this node.
pub requests_served: u64,
/// Completion tokens produced.
pub tokens_served: u64,
/// Recent decode throughput.
pub tokens_per_second: f64,
/// Requests served for this machine's own agents.
pub local_attempts: u64,
/// Requests served for a remote peer (someone else consuming my compute).
pub remote_attempts: u64,
/// Requests served via an advertised endpoint (also a remote consumer).
pub endpoint_attempts: u64,
/// Other nodes currently visible as peers.
pub peers: u64,
}
/// Pure extractor: project a raw SDK status payload into [`MeshServingUsage`].
///
/// Every field is read defensively (missing → 0) so an SDK shape change
/// degrades to "no usage shown" rather than an error. Kept pure so it can be
/// unit-tested against a captured payload without a live runtime.
pub fn serving_usage_from_payload(payload: &serde_json::Value) -> MeshServingUsage {
let u64_at = |v: &serde_json::Value| v.as_u64().unwrap_or(0);
let rm = payload.get("routing_metrics");
let local = rm.and_then(|m| m.get("local_node"));
let get_u64 = |obj: Option<&serde_json::Value>, key: &str| {
obj.and_then(|o| o.get(key)).map(u64_at).unwrap_or(0)
};
MeshServingUsage {
inflight: local
.and_then(|l| l.get("current_inflight_requests"))
.map(u64_at)
.or_else(|| payload.get("inflight_requests").map(u64_at))
.unwrap_or(0),
peak_inflight: get_u64(local, "peak_inflight_requests"),
requests_served: get_u64(rm, "request_count"),
tokens_served: get_u64(rm, "completion_tokens_observed"),
tokens_per_second: rm
.and_then(|m| m.get("avg_tokens_per_second"))
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0),
local_attempts: get_u64(local, "local_attempt_count"),
remote_attempts: get_u64(local, "remote_attempt_count"),
endpoint_attempts: get_u64(local, "endpoint_attempt_count"),
peers: payload
.get("peers")
.and_then(serde_json::Value::as_array)
.map(|a| a.len() as u64)
.unwrap_or(0),
}
}

View File

@@ -92,7 +92,8 @@ export function PersonaModelField({
) : null}
{showSharedComputeAutoHint ? (
<p className="text-xs text-muted-foreground">
Buzz will choose an available shared model when the agent starts.
Auto uses Mesh collective intelligence when two or more models stay
available, otherwise it chooses one available model.
</p>
) : null}
{modelDiscoveryStatus ? (

View File

@@ -44,6 +44,16 @@ test("an explicit inherited default label wins over harness discovery", () => {
);
});
test("shared compute describes Auto's collective behavior", () => {
assert.equal(
resolveDefaultModelLabel({
discoveredModelOptions: null,
isSharedCompute: true,
}),
"Auto (collective when available)",
);
});
// ── appendNoModelsSentinel ─────────────────────────────────────────────────────
test("appendNoModelsSentinel_emptyOptionsDiscoveryFinished_addsDisabledRow", () => {

View File

@@ -326,7 +326,9 @@ export function resolveDefaultModelLabel({
return (
defaultModelLabel ??
discoveredModelOptions?.find((option) => option.id.trim() === "")?.label ??
(isSharedCompute ? "Default (auto)" : getDefaultLlmModelLabel(globalModel))
(isSharedCompute
? "Auto (collective when available)"
: getDefaultLlmModelLabel(globalModel))
);
}

View File

@@ -9,7 +9,7 @@ import { AUTO_MODEL_DROPDOWN_VALUE } from "./agentConfigOptions.tsx";
const fallback = [{ id: "", label: "Default model" }];
const live = [
{ id: "", label: "Default (auto)" },
{ id: "", label: "Auto (collective when available)" },
{ id: "mesh/model", label: "mesh/model" },
];
@@ -35,7 +35,9 @@ test("Buzz shared compute fallback is Default auto while normal providers remain
model: "",
provider: "relay-mesh",
});
assert.deepEqual(mesh.options, [{ id: "", label: "Default (auto)" }]);
assert.deepEqual(mesh.options, [
{ id: "", label: "Auto (collective when available)" },
]);
const openai = relayMeshModelPickerState({
discoveredOptions: null,
@@ -57,7 +59,9 @@ test("Buzz shared compute keeps Default auto when discovery is empty", () => {
provider: "relay-mesh",
});
assert.deepEqual(state.options, [{ id: "", label: "Default (auto)" }]);
assert.deepEqual(state.options, [
{ id: "", label: "Auto (collective when available)" },
]);
assert.equal(state.selectValue, AUTO_MODEL_DROPDOWN_VALUE);
assert.equal(state.showCustomInput, false);
});
@@ -71,6 +75,6 @@ test("Buzz shared compute dropdown contains Default plus live models and no cust
});
assert.deepEqual(
options.map((option) => option.label),
["Default (auto)", "mesh/model"],
["Auto (collective when available)", "mesh/model"],
);
});

View File

@@ -12,7 +12,10 @@ function withSharedComputeAutoOption(
options: readonly PersonaModelOption[],
): readonly PersonaModelOption[] {
const modelOptions = options.filter((option) => option.id.trim() !== "");
return [{ id: "", label: "Default (auto)" }, ...modelOptions];
return [
{ id: "", label: "Auto (collective when available)" },
...modelOptions,
];
}
export function relayMeshModelPickerState({

View File

@@ -21,13 +21,14 @@ export type MeshShareToggleModel = {
isSharing: boolean;
/**
* A client-mode runtime occupies the single slot (this machine is consuming
* a peer's compute). The Share switch must read off + disabled while true.
* a peer's compute). The Share switch reads off while true, but the member
* may replace this client with a serve runtime by turning sharing on.
*/
isConsuming: boolean;
/**
* ANY runtime occupies the single slot (serve or client, healthy or failed).
* A fresh `mesh_start_node` fails with "already running" while true, so the
* switch must not offer a start — only a stop of an existing serve node.
* This remains useful for disabling edits around an existing serve runtime;
* the backend supports the intentional client-to-serve replacement.
*/
slotOccupied: boolean;
};

View File

@@ -124,22 +124,21 @@ export function MeshComputeSettingsCard() {
};
}, []);
// Mirror the running node's modelId back into the field so the card shows
// what's ACTUALLY being served — not just on a fresh load (empty field) but
// whenever the served model differs from the field (e.g. the member switched
// models and the node restarted on the new one). This is safe from clobbering
// a mid-edit because the field is disabled whenever the slot is occupied
// (`controlsDisabled`), so the user can't be typing while a node is running.
// Mirror only a SERVE runtime's model into the field. A client reports the
// remote model it is consuming; copying that value here both loses the
// member's local sharing choice and can cause this machine to download the
// remote member's much larger model when sharing is enabled.
React.useEffect(() => {
if (
status?.state === "running" &&
status.mode === "serve" &&
status.modelId &&
status.modelId !== modelInput
) {
setModelInput(status.modelId);
writeDraft(MODEL_DRAFT_STORAGE_KEY, status.modelId);
}
}, [status?.state, status?.modelId, modelInput]);
}, [status?.state, status?.mode, status?.modelId, modelInput]);
// The Share toggle reflects ONLY serve-mode occupancy. A client-mode runtime
// (this machine consuming a peer's compute) shares the single runtime slot
@@ -151,14 +150,12 @@ export function MeshComputeSettingsCard() {
// actively sharing (serve mode). Read-only; reads the node's own metrics.
const servingUsage = useMeshServingUsage(isSharing);
const servingIndicator = deriveServingIndicator(servingUsage, isSharing);
// Any occupying runtime (serve or client, healthy or failed) locks the model
// inputs and blocks a fresh start — stop it before reconfiguring.
const controlsDisabled = slotOccupied || actionInFlight;
// A consuming client may be intentionally replaced by a serve runtime, so
// keep the local model controls available in client mode. Serve and unknown
// occupants remain locked until stopped/recovered.
const controlsDisabled = actionInFlight || (slotOccupied && !isConsuming);
const refClass = classifyModelRef(modelInput);
const canStart =
refClass.kind !== "unknown" &&
!actionInFlight &&
status?.state !== "starting";
const canStart = refClass.kind !== "unknown" && !actionInFlight;
async function handleToggle(next: boolean) {
// Never let the Share switch tear down a consume session. The switch is
@@ -260,13 +257,15 @@ export function MeshComputeSettingsCard() {
checked={isSharing}
data-testid="mesh-share-compute-toggle"
disabled={
// When the slot is occupied, the switch is only actionable if
// WE are sharing (so it can stop a serve node — even a failed
// one). Any other occupant (consuming, or an unexpected
// modeless-running node) would make a fresh start throw "already
// running", so keep it disabled. When the slot is empty, gate on
// a valid model ref.
actionInFlight || (slotOccupied ? !isSharing : !canStart)
// A serve node can always be stopped. An off node or consuming
// client can start sharing once a valid local model is selected.
// Unknown occupants remain protected from replacement.
actionInFlight ||
(isSharing
? false
: slotOccupied && !isConsuming
? true
: !canStart)
}
id="mesh-share-compute-toggle"
onCheckedChange={handleToggle}
@@ -563,14 +562,13 @@ function StatusLine({
return <p className="text-sm text-muted-foreground">Stopping…</p>;
}
// A client-mode runtime owns the single slot: this machine is consuming a
// peer's compute, not sharing. Explain why Share is off + disabled instead
// of showing the misleading "Sharing … with relay members" serve copy. The
// client node is app-session-lived infra with no user stop control, so the
// copy states the fact rather than promising an action that doesn't exist.
// peer's compute, not sharing. The switch stays off, but remains available
// so the member can replace the client with a serving runtime.
if (isConsuming) {
return (
<p className="text-sm text-muted-foreground">
This machine is currently using another member's shared compute.
This machine is currently using another member's shared compute. Turn on
sharing to switch to the selected local model; Buzz may briefly restart.
</p>
);
}

View File

@@ -5,19 +5,20 @@ import { openSettings } from "../helpers/settings";
type E2eWindow = Window & {
__BUZZ_E2E_COMMANDS__?: string[];
__BUZZ_E2E_COMMAND_PAYLOADS__?: Array<{
command: string;
payload: { request?: { mode?: string; modelId?: string } } | null;
}>;
__BUZZ_E2E_SET_MESH__?: (mesh: {
nodeState?: "off" | "running";
nodeMode?: "serve" | "client" | null;
}) => void;
};
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
test("Share compute has a clear empty state and starts and stops sharing", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await openSettings(page, "compute");
@@ -56,14 +57,20 @@ test("Share compute has a clear empty state and starts and stops sharing", async
.toContain("mesh_stop_node");
});
test("consuming a peer's compute does NOT light the Share toggle", async ({
test("a consuming client can switch to sharing its saved local model", async ({
page,
}) => {
// Regression: consuming someone else's shared compute starts a client-mode
// node in the single runtime slot, which reports state:"running". The Share
// toggle keyed off state alone and lit up — and clicking it would have torn
// down the unrelated consume session. It must stay off + disabled, explain
// why, and issue no stop command.
// toggle keyed off state alone and lit up. A later guard overcorrected by
// disabling the switch and copying the remote model over the local sharing
// choice. Keep the switch off, preserve the local model, then replace the
// client with one serve start (never a stop command).
const localModel = "hf://demo/local-small-model:Q4_K_M";
await page.addInitScript((model) => {
window.localStorage.setItem("buzz.mesh-compute.share.model.v1", model);
}, localModel);
await installMockBridge(page);
await page.goto("/");
// The mesh seed hook is installed when the mock bridge boots; calling it
// before then silently no-ops (optional chaining) and the seed is lost.
@@ -80,19 +87,26 @@ test("consuming a peer's compute does NOT light the Share toggle", async ({
const card = page.getByTestId("settings-mesh-share-compute");
const toggle = page.getByTestId("mesh-share-compute-toggle");
const model = page.getByTestId("mesh-share-compute-model");
await expect(card).toContainText(
"This machine is currently using another member's shared compute",
);
await expect(card).toContainText("Buzz may briefly restart");
await expect(toggle).not.toBeChecked();
await expect(toggle).toBeDisabled();
await expect(model).toBeEnabled();
await expect(model).toHaveValue(localModel);
await expect(toggle).toBeEnabled();
await toggle.click();
await expect(toggle).toBeChecked();
// The switch is disabled, so a click can't fire onCheckedChange — but assert
// the destructive command never went out regardless.
const stopIssued = await page.evaluate(() =>
((window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []).includes(
"mesh_stop_node",
),
);
expect(stopIssued).toBe(false);
const commands = await page.evaluate(() => ({
names: (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? [],
payloads: (window as E2eWindow).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [],
}));
expect(commands.names).not.toContain("mesh_stop_node");
expect(commands.payloads).toContainEqual({
command: "mesh_start_node",
payload: { request: { mode: "serve", modelId: localModel } },
});
});

View File

@@ -156,7 +156,7 @@ test("Buzz shared compute explains automatic model selection", async ({
await expect(page.locator("#persona-model")).toContainText("Automatic");
await expect(
page.getByText(
"Buzz will choose an available shared model when the agent starts.",
"Auto uses Mesh collective intelligence when two or more models stay available, otherwise it chooses one available model.",
),
).toBeVisible();
await expect(page.locator("#persona-custom-model")).toHaveCount(0);