From 96e0c7f1ea688dab1f903e7311385cc21ccc7f8b Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 18 Aug 2026 12:25:25 +1000 Subject: [PATCH 01/20] feat(mesh): upgrade to mesh-llm 0.76.0-rc3 and recommend Qwen3.8 27B Bump every mesh-llm git dependency in buzz-relay and buzz-desktop from v0.75.1 to v0.76.0-rc3, and make Qwen3.8 27B Q4_K_M the curated pick for 64GB-class machines. The rc carries the two changes that make Qwen3.8 safe to recommend: Mesh-LLM/mesh-llm#1283 routes Qwen3.8 identities to the qwen35 recurrent family (without it the identity compacts to a string matching neither qwen35 nor qwen36, so a hybrid recurrent model is classified as dense attention-KV), and #1342 restores recurrent shared prefixes. #1343 adds Qwen3.8-27B-Q4_K_M to the compiled MODEL_CATALOG. Because the large pick now exists in MODEL_CATALOG, the synthesized catalog entry Buzz carried for the gemma-4-26B pick is removed along with its size/file/description constants; the entry is canonicalized from the upstream catalog instead. The retired gemma-4-26B alias is kept in canonical_curated_model_id so machines that already selected it keep resolving to a model id Mesh's OpenAI ingress accepts. Dependency resolution notes: iroh moves 1.0.2 -> 1.0.3 to match the rc's requirement, and libsqlite3-sys moves 0.30.1 -> 0.35.0 because the rc's mesh-llm-log-store requires rusqlite 0.37 (only one package may link sqlite3). Verification: cargo check passes for buzz-relay and for buzz-desktop with --features mesh-llm; 2611 buzz-desktop tests pass; cargo fmt --check is clean. Two failures predate this change and reproduce on unmodified origin/main: the global_config inherited_shared_compute_translates_to_supported_agent_transport assertion (expects "auto", gets "mesh") and an items-after-test-module clippy lint in managed_agents/restore.rs. Not runtime-verified against a live Qwen3.8 mesh node from this branch. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- crates/buzz-relay/Cargo.toml | 4 +- desktop/src-tauri/Cargo.lock | 354 +++++++++++----------- desktop/src-tauri/Cargo.toml | 14 +- desktop/src-tauri/src/mesh_llm/catalog.rs | 49 ++- 4 files changed, 206 insertions(+), 215 deletions(-) diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml index deb2e7e16a5..b5844d996cc 100644 --- a/crates/buzz-relay/Cargo.toml +++ b/crates/buzz-relay/Cargo.toml @@ -86,8 +86,8 @@ async-compression = { version = "0.4.42", features = ["tokio", "gzip"] } dev = ["buzz-auth/dev"] [dev-dependencies] -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } -mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } +mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } +mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } # Relay-driven mesh lifecycle smoke (examples/mesh_relay_lifecycle_smoke.rs): # the relay client for discovery notes and the exact ed25519 the mesh owner # keys use for binding verification. diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 5bd19ef2b6d..5dbe746260d 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -210,7 +210,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -221,7 +221,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1474,9 +1474,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -1639,7 +1639,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -2306,7 +2306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.118", ] [[package]] @@ -2484,7 +2484,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2765,7 +2765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3283,7 +3283,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", + "windows-link 0.2.1", "windows-result 0.4.1", ] @@ -4357,14 +4357,14 @@ dependencies = [ [[package]] name = "iroh" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fca9b4b462c343ff88fc0af4096c186f939b602a0bc08723536ef2c31c93971" +checksum = "460de6bc52163b41b1646931f2897e5ab986f0966ade444467fec25024751a72" dependencies = [ "backon", "blake3", "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "ctutils", "data-encoding", "derive_more", @@ -4408,9 +4408,9 @@ dependencies = [ [[package]] name = "iroh-base" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830a582cd54410dc1aa71d4786a82c3297d7b0165accd8b6dbbb3b240b48140d" +checksum = "6be73e16ee21c923aca9b3121aaa0db936f7c7ecc156ff47b8dac944c68d59a8" dependencies = [ "curve25519-dalek 5.0.0-rc.0", "data-encoding", @@ -4427,12 +4427,12 @@ dependencies = [ [[package]] name = "iroh-dns" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516e4eedc38e33ab69a6bd325520332dc3d67b25454e2d590ebb84a25240dd9a" +checksum = "46f6a9b39d18e6345f5c151afd299f2488e2cb5c520fe41b107b6bd3dc4c3349" dependencies = [ "arc-swap", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "derive_more", "hickory-resolver", "iroh-base", @@ -4478,13 +4478,13 @@ dependencies = [ [[package]] name = "iroh-relay" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8149bb6a57126225a07d6928846d82dcedfd24ea0f863ef7b2eb475e1d726354" +checksum = "24bd586cf927f7b700f56ec3639b53cb5fa901ce284784051ff71092bfbf8193" dependencies = [ "blake3", "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "data-encoding", "derive_more", "getrandom 0.4.3", @@ -4517,7 +4517,6 @@ dependencies = [ "tokio-websockets", "tracing", "url", - "vergen-gitcl", "webpki-roots 1.0.8", "ws_stream_wasm", ] @@ -5158,6 +5157,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "memmem" version = "0.1.1" @@ -5175,8 +5183,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "hex", "mesh-llm-client", @@ -5185,8 +5193,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5196,13 +5204,13 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" [[package]] name = "mesh-llm-client" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "async-trait", @@ -5233,8 +5241,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "dirs", @@ -5249,8 +5257,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -5259,20 +5267,25 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", + "chrono", "clap", "crossterm 0.28.1", + "libc", "ratatui", + "serde", "serde_json", + "tracing", + "uuid", ] [[package]] name = "mesh-llm-gpu-bench" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "serde", "serde_json", @@ -5281,8 +5294,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "serde", "serde_json", @@ -5290,8 +5303,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "mesh-llm-native-runtime", ] @@ -5327,8 +5340,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "argon2", @@ -5362,6 +5375,7 @@ dependencies = [ "mesh-llm-guardrails", "mesh-llm-hf-hub", "mesh-llm-identity", + "mesh-llm-log-store", "mesh-llm-native-runtime", "mesh-llm-node", "mesh-llm-plugin", @@ -5415,14 +5429,16 @@ dependencies = [ "tracing-subscriber", "url", "urlencoding", + "uuid", + "windows-sys 0.61.2", "zeroize", "zip 2.4.2", ] [[package]] name = "mesh-llm-identity" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "argon2", "base64 0.22.1", @@ -5441,10 +5457,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mesh-llm-log-store" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +dependencies = [ + "chrono", + "data-encoding", + "hex", + "mesh-llm-events", + "rusqlite", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.18", + "uuid", + "windows-sys 0.61.2", +] + [[package]] name = "mesh-llm-native-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "serde", @@ -5454,8 +5488,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "mesh-llm-types", @@ -5468,8 +5502,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "async-trait", @@ -5485,8 +5519,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "dirs", @@ -5504,8 +5538,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "hex", @@ -5517,8 +5551,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "hex", "sha2 0.10.9", @@ -5526,16 +5560,16 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "iroh", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "dirs", @@ -5557,8 +5591,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5572,8 +5606,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "dirs", @@ -5583,8 +5617,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "chrono", @@ -5610,8 +5644,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "hex", "serde", @@ -5621,13 +5655,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" [[package]] name = "mesh-mixture-of-agents" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5640,18 +5674,19 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "libloading 0.8.9", "mesh-native-serving-plugin-api", "skippy-server", + "skippy-tokenizer", ] [[package]] @@ -5741,13 +5776,13 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "model-artifact" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "async-trait", @@ -5757,8 +5792,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "async-trait", @@ -5775,8 +5810,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "bytes", @@ -5795,16 +5830,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "model-artifact", @@ -5900,7 +5935,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5936,7 +5971,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "derive_more", "futures-buffered", "futures-lite", @@ -6126,7 +6161,7 @@ checksum = "4d9cbe01741347ef750d743d6690603f5eed8341e679fb51c8e629337aa11976" dependencies = [ "atomic-waker", "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "derive_more", "ipnet", "js-sys", @@ -6181,7 +6216,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.13.0", "cfg-if 1.0.4", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", "memoffset", ] @@ -6194,7 +6229,7 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.13.0", "cfg-if 1.0.4", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", ] @@ -6206,7 +6241,7 @@ checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags 2.13.0", "cfg-if 1.0.4", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", ] @@ -6231,12 +6266,12 @@ dependencies = [ [[package]] name = "noq" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf95190af1bd4a00a10e8255ca0c8ddd9e9a9f5e79151d7a7eb6d56aff5dc89" +checksum = "09e4bb6601fa543c110d8957813267d5a8d775a0f8fbaccf1f615d06ba9b10da" dependencies = [ "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "derive_more", "noq-proto", "noq-udp", @@ -6253,9 +6288,9 @@ dependencies = [ [[package]] name = "noq-proto" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6c890013591e709a3e45dd53501351b7e27e7ff3c7e9fc3dce43e300e7e9d3" +checksum = "baa7b5ccd819a9c68a0d955e67a881032d09b1a17219b1f90b0997a0888e1a15" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -6281,11 +6316,11 @@ dependencies = [ [[package]] name = "noq-udp" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3137a52df66c20090a889828d1c655f21f52294cba64e5c4fbb04fc83eee7c8e" +checksum = "02bba20e097a5a16cd0ad14ec882fae1e80a092a124e9422fc4dddd92e96a647" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", "socket2", "tracing", @@ -6413,7 +6448,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6905,19 +6940,21 @@ dependencies = [ [[package]] name = "openai-frontend" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "async-trait", "axum", "futures-core", "futures-util", + "mesh-llm-events", "mesh-llm-guardrails", "serde", "serde_json", "tokio", "tokio-stream", "tracing", + "uuid", ] [[package]] @@ -7117,7 +7154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -8096,7 +8133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -8138,12 +8175,12 @@ version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases 0.2.2", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8874,7 +8911,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8944,7 +8981,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9211,7 +9248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9696,39 +9733,43 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skippy-cache" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "blake3", + "libc", + "memmap2", + "serde", + "serde_json", "skippy-protocol", ] [[package]] name = "skippy-coordinator" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "libloading 0.8.9", ] [[package]] name = "skippy-metrics" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" [[package]] name = "skippy-protocol" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "prost 0.14.4", "prost-build 0.14.4", @@ -9739,8 +9780,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "anyhow", "libc", @@ -9753,8 +9794,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "ahash", "anyhow", @@ -9777,24 +9818,26 @@ dependencies = [ "skippy-protocol", "skippy-runtime", "skippy-tokenizer", + "skippy-topology", "socket2", "tokio", "tokio-stream", "tonic", + "uuid", ] [[package]] name = "skippy-tokenizer" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc3" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" dependencies = [ "serde", "serde_json", @@ -9830,7 +9873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10844,10 +10887,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -10869,7 +10912,7 @@ dependencies = [ "parking_lot", "rustix 1.1.4", "signal-hook 0.3.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11611,7 +11654,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11712,7 +11755,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11975,43 +12018,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vergen" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", - "vergen-lib", -] - -[[package]] -name = "vergen-gitcl" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", - "time", - "vergen", - "vergen-lib", -] - -[[package]] -name = "vergen-lib" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", -] - [[package]] name = "version-compare" version = "0.2.1" @@ -12563,7 +12569,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index db089fc13fa..d599670bf18 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -110,15 +110,15 @@ buzz_voice_pkg = { package = "buzz-voice", path = "../../crates/buzz-voice" } buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" } buzz_ws_client_pkg = { package = "buzz-ws-client", path = "../../crates/buzz-ws-client" } portable-pty = "0.9" -iroh = { version = "1.0.2", optional = true } -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", 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", tag = "v0.75.1", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true } +iroh = { version = "1.0.3", optional = true } +mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", 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", tag = "v0.76.0-rc3", 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", tag = "v0.75.1", package = "mesh-llm-client", optional = true } -mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-node", optional = true } -mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-system", optional = true } -mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-events", optional = true } +mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-client", optional = true } +mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-node", optional = true } +mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-system", optional = true } +mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-events", optional = true } base64 = "0.22" sha2 = "0.11" tar = "0.4" diff --git a/desktop/src-tauri/src/mesh_llm/catalog.rs b/desktop/src-tauri/src/mesh_llm/catalog.rs index 1a11fcfcd13..72da0c170f4 100644 --- a/desktop/src-tauri/src/mesh_llm/catalog.rs +++ b/desktop/src-tauri/src/mesh_llm/catalog.rs @@ -13,20 +13,20 @@ use mesh_llm_system::hardware; use mesh_llm_system::vram::{format_rated_capacity, rated_capacity_gb}; /// Buzz-curated tier picks. These are the models we know survive the agent -/// harness on shared compute — deliberately non-reasoning instruction models, -/// so agents stay snappy instead of burning hidden reasoning tokens. +/// harness on shared compute. /// -/// The large pick is resolved through mesh-llm's remote catalog -/// (huggingface.co/datasets/meshllm/catalog), so it does not need to exist in -/// the compiled `MODEL_CATALOG`; the entry is synthesized below. -const CURATED_LARGE: &str = "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M"; -const CURATED_LARGE_ALIAS: &str = "gemma-4-26B-A4B-it-UD-Q4_K_M"; -const CURATED_LARGE_SIZE: &str = "17GB"; -const CURATED_LARGE_FILE: &str = "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"; -const CURATED_LARGE_DESCRIPTION: &str = - "Gemma 4 26B MoE (4B active) — Buzz default for 64GB+ machines"; +/// The large pick is Qwen3.8 27B dense: mesh-llm 0.76 routes Qwen3.8 +/// identities to the `qwen35` recurrent family (Mesh-LLM/mesh-llm#1283) and +/// ships it in the compiled `MODEL_CATALOG` (#1343), so the entry below is +/// canonicalized from that catalog rather than synthesized. +const CURATED_LARGE: &str = "unsloth/Qwen3.8-27B-GGUF:Q4_K_M"; +const CURATED_LARGE_ALIAS: &str = "Qwen3.8-27B-Q4_K_M"; const CURATED_SMALL: &str = "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M"; const CURATED_SMALL_ALIAS: &str = "Gemma-4-E4B-it-Q4_K_M"; +/// Superseded large pick, still mapped so machines that already selected it +/// keep resolving to a model id Mesh's OpenAI ingress accepts. +const LEGACY_LARGE: &str = "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M"; +const LEGACY_LARGE_ALIAS: &str = "gemma-4-26B-A4B-it-UD-Q4_K_M"; /// Rated-capacity boundary between the two curated tiers, in GB (marketing /// capacity — a "64GB" Mac rates as 64 even though usable AI memory is less). const CURATED_LARGE_MIN_RATED_GB: u64 = 64; @@ -39,12 +39,13 @@ fn buzz_recommended_model(rated_gb: Option) -> &'static str { } } -/// Convert Buzz's pre-0.74 curated package aliases into the canonical model +/// Convert Buzz's historical curated package aliases into the canonical model /// ids advertised and accepted by Mesh's OpenAI ingress. pub(crate) fn canonical_curated_model_id(model_id: &str) -> &str { match model_id.trim() { CURATED_SMALL_ALIAS => CURATED_SMALL, CURATED_LARGE_ALIAS => CURATED_LARGE, + LEGACY_LARGE_ALIAS => LEGACY_LARGE, other => other, } } @@ -172,24 +173,6 @@ fn build_catalog( }) .collect(); - // The compiled MODEL_CATALOG does not know the Buzz large pick; it - // resolves through mesh-llm's remote catalog at download time. Synthesize - // its entry so the picker can offer it. - if !entries.iter().any(|e| e.name == CURATED_LARGE) { - let size_gb = parse_size_gb(CURATED_LARGE_SIZE); - entries.push(MeshCatalogEntry { - fit: fit_code(size_gb, vram_gb), - installed: is_installed(CURATED_LARGE_FILE, CURATED_LARGE) - || is_installed(CURATED_LARGE_FILE, CURATED_LARGE_ALIAS), - recommended: false, - curated: false, - name: CURATED_LARGE.to_string(), - size: CURATED_LARGE_SIZE.to_string(), - size_gb, - description: CURATED_LARGE_DESCRIPTION.to_string(), - }); - } - let recommended = Some(buzz_recommended_model(rated_capacity_gb(vram_bytes)).to_string()); for entry in &mut entries { entry.recommended = recommended.as_deref() == Some(entry.name.as_str()); @@ -271,7 +254,7 @@ mod tests { #[test] fn recommendation_follows_buzz_curated_tiers() { assert_eq!(CURATED_SMALL, "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M"); - assert_eq!(CURATED_LARGE, "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M"); + assert_eq!(CURATED_LARGE, "unsloth/Qwen3.8-27B-GGUF:Q4_K_M"); // 64GB+ rated machines get the large curated pick. let large = build_catalog(None, 64_000_000_000, 64.0, &[]); assert_eq!(large.recommended.as_deref(), Some(CURATED_LARGE)); @@ -295,6 +278,7 @@ mod tests { canonical_curated_model_id(CURATED_LARGE_ALIAS), CURATED_LARGE ); + assert_eq!(canonical_curated_model_id(LEGACY_LARGE_ALIAS), LEGACY_LARGE); assert_eq!( canonical_curated_model_id("other/model:Q4"), "other/model:Q4" @@ -311,7 +295,8 @@ mod tests { assert_eq!(catalog.entries[1].name, CURATED_SMALL); assert!(catalog.entries[1].curated && !catalog.entries[1].recommended); assert!(catalog.entries[2..].iter().all(|e| !e.curated)); - // The synthesized large pick carries a real size for fit ranking. + // The large pick comes from the compiled catalog with a real size, so + // fit ranking is meaningful rather than a placeholder. assert!(catalog.entries[0].size_gb > 10.0); } From 0eecc0ca20885ccc7ea557746761ce1d2a0de7ab Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Mon, 17 Aug 2026 20:59:05 +1000 Subject: [PATCH 02/20] fix(desktop): stop abandoning healthy mesh prefills at four minutes buzz-agent's `BUZZ_AGENT_LLM_TIMEOUT_SECS` defaults to 240s. MeshLLM's OpenAI frontend gives a backend call 600s (`OpenAiFrontendConfig::DEFAULT_BACKEND_TIMEOUT`), so on shared compute the client aborts work the server is still legitimately doing. Requests to a local mesh are `stream: false`, so a cold multi-ten-thousand token prefill is one silent request. Measured on an M5 Max serving Qwen3.8-27B Q4_K_M: an 88,318-token cold prompt returns HTTP 200 after 503s at 176 tok/s prefill. At the 240s default the client gives up at four minutes, then retries with an escalated budget (240 -> 480 -> 960), piling load onto a box that is already prefilling the first attempt. Seat the mesh client budget at 660s, just above the frontend's 600s, so the mesh's own error surfaces rather than a client abort racing it. Remote providers are untouched and keep buzz-agent's 240s default. Like the other mesh seeds this is a default, not policy: an explicit user value survives via `insert_default_if_unset` plus the copy-forward list in `relay_mesh_process_env`. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- .../src/managed_agents/relay_mesh.rs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/desktop/src-tauri/src/managed_agents/relay_mesh.rs b/desktop/src-tauri/src/managed_agents/relay_mesh.rs index 3858212bbba..b6d5e33f309 100644 --- a/desktop/src-tauri/src/managed_agents/relay_mesh.rs +++ b/desktop/src-tauri/src/managed_agents/relay_mesh.rs @@ -66,6 +66,21 @@ pub fn apply_relay_mesh_env( // survives (see `insert_default_if_unset`, and the copy-forward list in // `relay_mesh_process_env` that preserves it through the spawn path). insert_default_if_unset(env, "BUZZ_AGENT_REQUIRE_REPLY", "1"); + // A local mesh prefills a cold multi-ten-thousand-token prompt at a few + // hundred tokens/second, so the first turn of a large session legitimately + // runs for minutes with no bytes on the wire (`stream: false`). MeshLLM's + // own frontend gives such a request 600 s before it aborts + // (`OpenAiFrontendConfig::DEFAULT_BACKEND_TIMEOUT`); buzz-agent's 240 s + // default abandons it at four minutes, so the client gives up on work the + // server is still doing and retries it — adding load to a box that is + // already prefilling. Seat the client just above the server's budget so the + // mesh's own error is what surfaces, rather than a client-side abort racing + // it. Measured: an 88,318-token cold prompt on an M5 Max returns 200 at + // 503 s. Remote providers keep the 240 s default; only mesh moves. + // A default, not policy — an explicit user value survives via + // `insert_default_if_unset` and the copy-forward list in + // `relay_mesh_process_env`. + insert_default_if_unset(env, "BUZZ_AGENT_LLM_TIMEOUT_SECS", "660"); // Deliberately no BUZZ_AGENT_THINKING_EFFORT default: mesh translates // `reasoning_effort` into the chat template's `enable_thinking` flag, so any // value we pick overrides each model's own template default — and the right @@ -102,6 +117,10 @@ pub fn relay_mesh_process_env( for key in [ "BUZZ_AGENT_MAX_OUTPUT_TOKENS", "BUZZ_AGENT_THINKING_EFFORT", + // Same reason as `BUZZ_AGENT_REQUIRE_REPLY` below: without the + // copy-forward, an explicit user timeout is re-defaulted to 660 by + // `apply_relay_mesh_env`. + "BUZZ_AGENT_LLM_TIMEOUT_SECS", // Must be copied forward for the user's value to survive: this map is // written onto the command *after* the layered user env, so a key absent // here is re-defaulted by `apply_relay_mesh_env` below and an explicit @@ -270,6 +289,74 @@ mod tests { ); } + /// MeshLLM's frontend allows a backend call 600 s + /// (`OpenAiFrontendConfig::DEFAULT_BACKEND_TIMEOUT`). buzz-agent's 240 s + /// default abandons a healthy cold prefill long before the server does and + /// retries it, so the client budget must sit above the server's. + #[test] + fn native_provider_outlasts_the_mesh_backend_timeout() { + let mut env = BTreeMap::new(); + apply_relay_mesh_env( + &mut env, + Some(RELAY_MESH_PROVIDER_ID), + Some(RELAY_MESH_AUTO_MODEL_ID), + ); + + let seconds: u64 = env + .get("BUZZ_AGENT_LLM_TIMEOUT_SECS") + .expect("mesh seeds a client timeout") + .parse() + .expect("timeout is whole seconds"); + assert!( + seconds > 600, + "client budget ({seconds}s) must outlast the mesh frontend's 600s backend timeout" + ); + } + + #[test] + fn native_provider_preserves_explicit_llm_timeout() { + let mut env = + BTreeMap::from([("BUZZ_AGENT_LLM_TIMEOUT_SECS".to_string(), "90".to_string())]); + apply_relay_mesh_env( + &mut env, + Some(RELAY_MESH_PROVIDER_ID), + Some(RELAY_MESH_AUTO_MODEL_ID), + ); + + assert_eq!( + env.get("BUZZ_AGENT_LLM_TIMEOUT_SECS").map(String::as_str), + Some("90"), + "an explicit timeout is a user decision, not a value to re-default" + ); + } + + /// Same spawn-ordering hazard as the reply guard: without the copy-forward + /// in `relay_mesh_process_env`, the user's value is re-defaulted here. + #[test] + fn process_env_preserves_explicit_llm_timeout() { + let effective_env = + BTreeMap::from([("BUZZ_AGENT_LLM_TIMEOUT_SECS".to_string(), "90".to_string())]); + + let env = relay_mesh_process_env(&effective_env, "Gemma-4"); + + assert_eq!( + env.get("BUZZ_AGENT_LLM_TIMEOUT_SECS").map(String::as_str), + Some("90") + ); + } + + #[test] + fn non_mesh_provider_leaves_llm_timeout_unset() { + let mut env = BTreeMap::new(); + apply_relay_mesh_env(&mut env, Some("anthropic"), Some("claude-haiku-4.5")); + + assert_eq!( + env.get("BUZZ_AGENT_LLM_TIMEOUT_SECS"), + None, + "remote providers keep buzz-agent's own default" + ); + } + #[test] fn non_mesh_provider_leaves_reply_guard_unset() { let mut env = BTreeMap::new(); From 0cd224c2e251e72a69f217f9739dcb20ed271d46 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 20 Aug 2026 10:52:50 +1000 Subject: [PATCH 03/20] feat(mesh): move mesh-llm dependency to 0.76.0-rc4 Bump every mesh-llm git dependency in buzz-relay and buzz-desktop from the v0.76.0-rc3 tag to v0.76.0-rc4. rc4 is a low-risk move from rc3: git diff v0.76.0-rc3..v0.76.0-rc4 in Mesh-LLM/mesh-llm touches only generated console web assets and the release version bump, so the runtime behaviour Buzz depends on is the same code we already validated. It is the first release tag carrying the Qwen3.8 fixes as a downloadable artifact for every platform, which means nodes no longer need a source build to serve the curated large pick. The Qwen3.8 27B curated recommendation from the rc3 commit is unchanged. Verification: cargo check passes for buzz-relay and for buzz-desktop with --features mesh-llm. The one warning emitted (unreachable statement in commands/workspace.rs) predates this change. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- Cargo.lock | 372 +++++++++++++++++++---------------- crates/buzz-relay/Cargo.toml | 4 +- desktop/src-tauri/Cargo.lock | 204 +++++++++---------- desktop/src-tauri/Cargo.toml | 12 +- 4 files changed, 317 insertions(+), 275 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16d86d0206f..4b9a0baf6e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,7 +117,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -128,7 +128,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1566,9 +1566,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -1712,7 +1712,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2333,7 +2333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2544,7 +2544,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2767,7 +2767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2839,6 +2839,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy-regex" version = "0.11.0" @@ -3183,8 +3195,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", - "windows-result 0.4.1", + "windows-link 0.2.1", + "windows-result 0.3.4", ] [[package]] @@ -3407,6 +3419,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "hashlink" version = "0.11.0" @@ -3789,7 +3810,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -4083,9 +4104,9 @@ dependencies = [ [[package]] name = "iroh" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fca9b4b462c343ff88fc0af4096c186f939b602a0bc08723536ef2c31c93971" +checksum = "460de6bc52163b41b1646931f2897e5ab986f0966ade444467fec25024751a72" dependencies = [ "backon", "blake3", @@ -4134,9 +4155,9 @@ dependencies = [ [[package]] name = "iroh-base" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830a582cd54410dc1aa71d4786a82c3297d7b0165accd8b6dbbb3b240b48140d" +checksum = "6be73e16ee21c923aca9b3121aaa0db936f7c7ecc156ff47b8dac944c68d59a8" dependencies = [ "curve25519-dalek 5.0.0-rc.0", "data-encoding", @@ -4153,9 +4174,9 @@ dependencies = [ [[package]] name = "iroh-dns" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516e4eedc38e33ab69a6bd325520332dc3d67b25454e2d590ebb84a25240dd9a" +checksum = "46f6a9b39d18e6345f5c151afd299f2488e2cb5c520fe41b107b6bd3dc4c3349" dependencies = [ "arc-swap", "cfg_aliases", @@ -4204,9 +4225,9 @@ dependencies = [ [[package]] name = "iroh-relay" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8149bb6a57126225a07d6928846d82dcedfd24ea0f863ef7b2eb475e1d726354" +checksum = "24bd586cf927f7b700f56ec3639b53cb5fa901ce284784051ff71092bfbf8193" dependencies = [ "blake3", "bytes", @@ -4243,7 +4264,6 @@ dependencies = [ "tokio-websockets", "tracing", "url", - "vergen-gitcl", "webpki-roots 1.0.7", "ws_stream_wasm", ] @@ -4575,10 +4595,11 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -4818,6 +4839,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "memmem" version = "0.1.1" @@ -4835,8 +4865,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "hex", "mesh-llm-client", @@ -4845,8 +4875,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -4856,13 +4886,13 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "mesh-llm-client" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -4893,8 +4923,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -4909,8 +4939,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -4919,20 +4949,25 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", + "chrono", "clap", "crossterm 0.28.1", + "libc", "ratatui", + "serde", "serde_json", + "tracing", + "uuid", ] [[package]] name = "mesh-llm-gpu-bench" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", "serde_json", @@ -4941,8 +4976,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", "serde_json", @@ -4950,8 +4985,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "mesh-llm-native-runtime", ] @@ -4987,8 +5022,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "argon2", @@ -5022,6 +5057,7 @@ dependencies = [ "mesh-llm-guardrails", "mesh-llm-hf-hub", "mesh-llm-identity", + "mesh-llm-log-store", "mesh-llm-native-runtime", "mesh-llm-node", "mesh-llm-plugin", @@ -5075,14 +5111,16 @@ dependencies = [ "tracing-subscriber", "url", "urlencoding", + "uuid", + "windows-sys 0.61.2", "zeroize", "zip", ] [[package]] name = "mesh-llm-identity" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "argon2", "base64 0.22.1", @@ -5101,10 +5139,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mesh-llm-log-store" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +dependencies = [ + "chrono", + "data-encoding", + "hex", + "mesh-llm-events", + "rusqlite", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.18", + "uuid", + "windows-sys 0.61.2", +] + [[package]] name = "mesh-llm-native-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "serde", @@ -5114,8 +5170,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-types", @@ -5128,8 +5184,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5145,8 +5201,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5164,8 +5220,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "hex", @@ -5177,8 +5233,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "hex", "sha2 0.10.9", @@ -5186,16 +5242,16 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "iroh", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5217,8 +5273,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5232,8 +5288,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5243,8 +5299,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "chrono", @@ -5270,8 +5326,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "hex", "serde", @@ -5281,13 +5337,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "mesh-mixture-of-agents" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5300,18 +5356,19 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "libloading", "mesh-native-serving-plugin-api", "skippy-server", + "skippy-tokenizer", ] [[package]] @@ -5451,8 +5508,8 @@ dependencies = [ [[package]] name = "model-artifact" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5462,8 +5519,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5480,8 +5537,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "bytes", @@ -5500,16 +5557,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "model-artifact", @@ -5860,9 +5917,9 @@ dependencies = [ [[package]] name = "noq" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf95190af1bd4a00a10e8255ca0c8ddd9e9a9f5e79151d7a7eb6d56aff5dc89" +checksum = "09e4bb6601fa543c110d8957813267d5a8d775a0f8fbaccf1f615d06ba9b10da" dependencies = [ "bytes", "cfg_aliases", @@ -5882,9 +5939,9 @@ dependencies = [ [[package]] name = "noq-proto" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6c890013591e709a3e45dd53501351b7e27e7ff3c7e9fc3dce43e300e7e9d3" +checksum = "baa7b5ccd819a9c68a0d955e67a881032d09b1a17219b1f90b0997a0888e1a15" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -5910,9 +5967,9 @@ dependencies = [ [[package]] name = "noq-udp" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3137a52df66c20090a889828d1c655f21f52294cba64e5c4fbb04fc83eee7c8e" +checksum = "02bba20e097a5a16cd0ad14ec882fae1e80a092a124e9422fc4dddd92e96a647" dependencies = [ "cfg_aliases", "libc", @@ -6028,7 +6085,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6310,19 +6367,21 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" [[package]] name = "openai-frontend" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "async-trait", "axum", "futures-core", "futures-util", + "mesh-llm-events", "mesh-llm-guardrails", "serde", "serde_json", "tokio", "tokio-stream", "tracing", + "uuid", ] [[package]] @@ -7501,7 +7560,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -8102,6 +8161,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.10.0", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -8172,7 +8245,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8185,7 +8258,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8244,7 +8317,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8527,7 +8600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8887,39 +8960,43 @@ checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "skippy-cache" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "blake3", + "libc", + "memmap2", + "serde", + "serde_json", "skippy-protocol", ] [[package]] name = "skippy-coordinator" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "libloading", ] [[package]] name = "skippy-metrics" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "skippy-protocol" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "prost 0.14.3", "prost-build 0.14.3", @@ -8930,8 +9007,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "libc", @@ -8944,8 +9021,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "ahash", "anyhow", @@ -8968,24 +9045,26 @@ dependencies = [ "skippy-protocol", "skippy-runtime", "skippy-tokenizer", + "skippy-topology", "socket2", "tokio", "tokio-stream", "tonic", + "uuid", ] [[package]] name = "skippy-tokenizer" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.75.1" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", "serde_json", @@ -9024,7 +9103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9122,7 +9201,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink", + "hashlink 0.11.0", "indexmap", "log", "memchr", @@ -9652,7 +9731,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9665,7 +9744,7 @@ dependencies = [ "parking_lot", "rustix 1.1.4", "signal-hook", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10435,7 +10514,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10643,43 +10722,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vergen" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", - "vergen-lib", -] - -[[package]] -name = "vergen-gitcl" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", - "time", - "vergen", - "vergen-lib", -] - -[[package]] -name = "vergen-lib" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", -] - [[package]] name = "version_check" version = "0.9.5" @@ -11019,7 +11061,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -11504,8 +11546,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.62.2", - "windows-core 0.62.2", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml index b5844d996cc..6b1c0497271 100644 --- a/crates/buzz-relay/Cargo.toml +++ b/crates/buzz-relay/Cargo.toml @@ -86,8 +86,8 @@ async-compression = { version = "0.4.42", features = ["tokio", "gzip"] } dev = ["buzz-auth/dev"] [dev-dependencies] -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } -mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } +mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } +mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } # Relay-driven mesh lifecycle smoke (examples/mesh_relay_lifecycle_smoke.rs): # the relay client for discovery notes and the exact ed25519 the mesh owner # keys use for binding verification. diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 5dbe746260d..a8195bbe711 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -976,7 +976,7 @@ version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ - "darling 0.23.0", + "darling 0.20.11", "ident_case", "prettyplease", "proc-macro2", @@ -1639,7 +1639,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -1877,7 +1877,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.62.2", + "windows 0.61.3", ] [[package]] @@ -3283,8 +3283,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", + "windows-link 0.1.3", + "windows-result 0.3.4", ] [[package]] @@ -4031,7 +4031,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.6.1", + "windows-registry 0.5.3", ] [[package]] @@ -4046,7 +4046,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -5183,8 +5183,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "hex", "mesh-llm-client", @@ -5193,8 +5193,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5204,13 +5204,13 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "mesh-llm-client" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5241,8 +5241,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5257,8 +5257,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -5267,8 +5267,8 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "chrono", @@ -5284,8 +5284,8 @@ dependencies = [ [[package]] name = "mesh-llm-gpu-bench" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", "serde_json", @@ -5294,8 +5294,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", "serde_json", @@ -5303,8 +5303,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "mesh-llm-native-runtime", ] @@ -5340,8 +5340,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "argon2", @@ -5437,8 +5437,8 @@ dependencies = [ [[package]] name = "mesh-llm-identity" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "argon2", "base64 0.22.1", @@ -5459,8 +5459,8 @@ dependencies = [ [[package]] name = "mesh-llm-log-store" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "chrono", "data-encoding", @@ -5477,8 +5477,8 @@ dependencies = [ [[package]] name = "mesh-llm-native-runtime" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "serde", @@ -5488,8 +5488,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-types", @@ -5502,8 +5502,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5519,8 +5519,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5538,8 +5538,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "hex", @@ -5551,8 +5551,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "hex", "sha2 0.10.9", @@ -5560,16 +5560,16 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "iroh", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5591,8 +5591,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5606,8 +5606,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "dirs", @@ -5617,8 +5617,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "chrono", @@ -5644,8 +5644,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "hex", "serde", @@ -5655,13 +5655,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "mesh-mixture-of-agents" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5674,13 +5674,13 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "libloading 0.8.9", @@ -5781,8 +5781,8 @@ dependencies = [ [[package]] name = "model-artifact" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5792,8 +5792,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "async-trait", @@ -5810,8 +5810,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "bytes", @@ -5830,16 +5830,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "model-artifact", @@ -6558,7 +6558,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.118", @@ -6940,8 +6940,8 @@ dependencies = [ [[package]] name = "openai-frontend" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "async-trait", "axum", @@ -7154,7 +7154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -7914,7 +7914,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools", "log", "multimap", @@ -7934,7 +7934,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools", "log", "multimap", @@ -9733,8 +9733,8 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skippy-cache" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "blake3", @@ -9747,29 +9747,29 @@ dependencies = [ [[package]] name = "skippy-coordinator" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "libloading 0.8.9", ] [[package]] name = "skippy-metrics" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" [[package]] name = "skippy-protocol" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "prost 0.14.4", "prost-build 0.14.4", @@ -9780,8 +9780,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "anyhow", "libc", @@ -9794,8 +9794,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "ahash", "anyhow", @@ -9828,16 +9828,16 @@ dependencies = [ [[package]] name = "skippy-tokenizer" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.76.0-rc3" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc3#d823a643243c84e81efc21e3a323c92195f8301a" +version = "0.76.0-rc4" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" dependencies = [ "serde", "serde_json", @@ -12569,7 +12569,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -13207,8 +13207,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.62.2", - "windows-core 0.62.2", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d599670bf18..35b04c70c18 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -111,14 +111,14 @@ buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" } buzz_ws_client_pkg = { package = "buzz-ws-client", path = "../../crates/buzz-ws-client" } portable-pty = "0.9" iroh = { version = "1.0.3", optional = true } -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", 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", tag = "v0.76.0-rc3", 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", tag = "v0.76.0-rc4", 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", tag = "v0.76.0-rc4", 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", tag = "v0.76.0-rc3", package = "mesh-llm-client", optional = true } -mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-node", optional = true } -mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-system", optional = true } -mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc3", package = "mesh-llm-events", optional = true } +mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-client", optional = true } +mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-node", optional = true } +mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-system", optional = true } +mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-events", optional = true } base64 = "0.22" sha2 = "0.11" tar = "0.4" From 92ecac29eac5747f0a72bb9a3709ebd537a07c70 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 20 Aug 2026 14:18:24 +1000 Subject: [PATCH 04/20] fix(dev): honor mesh=1 in desktop-standalone desktop-standalone was the only launch recipe that ignored the `mesh` variable. dev, staging and production all build FEATURES=(--features mesh-llm) when it is set; desktop-standalone invoked `tauri dev` with no features, so `just mesh=1 desktop-standalone` silently produced a non-mesh build and the app reported "mesh-llm feature not enabled" from desktop/src-tauri/src/mesh_llm_stubs.rs. Wire it up the same way as the other three recipes. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- Justfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Justfile b/Justfile index ce8647cf77c..fef033e96fc 100644 --- a/Justfile +++ b/Justfile @@ -548,7 +548,11 @@ desktop-standalone *ARGS: _ensure-sidecar-stubs fi trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT echo "Starting standalone desktop on Vite port ${BUZZ_VITE_PORT}; no relay services were started" - pnpm exec tauri dev --config "$BUZZ_TAURI_CONFIG" {{ARGS}} + FEATURES=() + if [[ -n "{{mesh}}" ]]; then + FEATURES=(--features mesh-llm) + fi + pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}} # Run the desktop app against the internal staging relay (installs deps + builds agent tools automatically) staging *ARGS: bootstrap _ensure-sidecar-stubs From a6b4756cc69bfdcaba8e23a464bd0240bc8e6da4 Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Fri, 21 Aug 2026 23:50:08 +1000 Subject: [PATCH 05/20] feat(mesh): move mesh-llm dependency to 0.76.0-rc6 Bump every mesh-llm git dependency in buzz-relay and buzz-desktop from the v0.76.0-rc4 tag to v0.76.0-rc6, and regenerate both lockfiles at tag commit a8ddb455. Verification: cargo check -p buzz-relay --all-targets and cargo check --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm --all-targets pass. The desktop check retains the pre-existing unreachable-code warning in commands/workspace.rs. The buzz-relay package suite compiled rc6 and ran 892 passing tests, but its existing mesh demo forwarded-arm test timed out with HTTP 504 twice. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- Cargo.lock | 190 ++++++++++++++++------------------- crates/buzz-relay/Cargo.toml | 4 +- desktop/src-tauri/Cargo.lock | 190 ++++++++++++++++------------------- desktop/src-tauri/Cargo.toml | 12 +-- 4 files changed, 186 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b9a0baf6e6..839355a70ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4839,15 +4839,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - [[package]] name = "memmem" version = "0.1.1" @@ -4865,8 +4856,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "hex", "mesh-llm-client", @@ -4875,8 +4866,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -4886,13 +4877,13 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "mesh-llm-client" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -4923,8 +4914,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -4939,8 +4930,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -4949,8 +4940,8 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "chrono", @@ -4966,8 +4957,8 @@ dependencies = [ [[package]] name = "mesh-llm-gpu-bench" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", "serde_json", @@ -4976,8 +4967,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", "serde_json", @@ -4985,8 +4976,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "mesh-llm-native-runtime", ] @@ -5022,8 +5013,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "argon2", @@ -5119,8 +5110,8 @@ dependencies = [ [[package]] name = "mesh-llm-identity" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "argon2", "base64 0.22.1", @@ -5141,8 +5132,8 @@ dependencies = [ [[package]] name = "mesh-llm-log-store" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "chrono", "data-encoding", @@ -5159,8 +5150,8 @@ dependencies = [ [[package]] name = "mesh-llm-native-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "serde", @@ -5170,8 +5161,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-types", @@ -5184,8 +5175,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5201,8 +5192,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5220,8 +5211,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "hex", @@ -5233,8 +5224,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "hex", "sha2 0.10.9", @@ -5242,16 +5233,16 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "iroh", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5273,8 +5264,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5288,8 +5279,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5299,8 +5290,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "chrono", @@ -5326,8 +5317,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "hex", "serde", @@ -5337,13 +5328,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "mesh-mixture-of-agents" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5356,13 +5347,13 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "libloading", @@ -5508,8 +5499,8 @@ dependencies = [ [[package]] name = "model-artifact" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5519,8 +5510,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5537,8 +5528,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "bytes", @@ -5557,16 +5548,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "model-artifact", @@ -6367,8 +6358,8 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" [[package]] name = "openai-frontend" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "async-trait", "axum", @@ -8960,43 +8951,39 @@ checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "skippy-cache" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "blake3", - "libc", - "memmap2", - "serde", - "serde_json", "skippy-protocol", ] [[package]] name = "skippy-coordinator" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "libloading", ] [[package]] name = "skippy-metrics" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "skippy-protocol" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "prost 0.14.3", "prost-build 0.14.3", @@ -9007,8 +8994,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "libc", @@ -9021,8 +9008,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "ahash", "anyhow", @@ -9033,6 +9020,7 @@ dependencies = [ "clap", "futures-util", "libc", + "mesh-llm-events", "mesh-native-serving-plugin-api", "model-artifact", "openai-frontend", @@ -9055,16 +9043,16 @@ dependencies = [ [[package]] name = "skippy-tokenizer" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", "serde_json", diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml index 6b1c0497271..d8a8288e1e7 100644 --- a/crates/buzz-relay/Cargo.toml +++ b/crates/buzz-relay/Cargo.toml @@ -86,8 +86,8 @@ async-compression = { version = "0.4.42", features = ["tokio", "gzip"] } dev = ["buzz-auth/dev"] [dev-dependencies] -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } -mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } +mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } +mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } # Relay-driven mesh lifecycle smoke (examples/mesh_relay_lifecycle_smoke.rs): # the relay client for discovery notes and the exact ed25519 the mesh owner # keys use for binding verification. diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index a8195bbe711..00465b579db 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -5157,15 +5157,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - [[package]] name = "memmem" version = "0.1.1" @@ -5183,8 +5174,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "hex", "mesh-llm-client", @@ -5193,8 +5184,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5204,13 +5195,13 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "mesh-llm-client" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5241,8 +5232,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5257,8 +5248,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -5267,8 +5258,8 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "chrono", @@ -5284,8 +5275,8 @@ dependencies = [ [[package]] name = "mesh-llm-gpu-bench" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", "serde_json", @@ -5294,8 +5285,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", "serde_json", @@ -5303,8 +5294,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "mesh-llm-native-runtime", ] @@ -5340,8 +5331,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "argon2", @@ -5437,8 +5428,8 @@ dependencies = [ [[package]] name = "mesh-llm-identity" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "argon2", "base64 0.22.1", @@ -5459,8 +5450,8 @@ dependencies = [ [[package]] name = "mesh-llm-log-store" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "chrono", "data-encoding", @@ -5477,8 +5468,8 @@ dependencies = [ [[package]] name = "mesh-llm-native-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "serde", @@ -5488,8 +5479,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-types", @@ -5502,8 +5493,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5519,8 +5510,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5538,8 +5529,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "hex", @@ -5551,8 +5542,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "hex", "sha2 0.10.9", @@ -5560,16 +5551,16 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "iroh", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5591,8 +5582,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5606,8 +5597,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "dirs", @@ -5617,8 +5608,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "chrono", @@ -5644,8 +5635,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "hex", "serde", @@ -5655,13 +5646,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "mesh-mixture-of-agents" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5674,13 +5665,13 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "libloading 0.8.9", @@ -5781,8 +5772,8 @@ dependencies = [ [[package]] name = "model-artifact" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5792,8 +5783,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "async-trait", @@ -5810,8 +5801,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "bytes", @@ -5830,16 +5821,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "model-artifact", @@ -6940,8 +6931,8 @@ dependencies = [ [[package]] name = "openai-frontend" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "async-trait", "axum", @@ -9733,43 +9724,39 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skippy-cache" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "blake3", - "libc", - "memmap2", - "serde", - "serde_json", "skippy-protocol", ] [[package]] name = "skippy-coordinator" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "libloading 0.8.9", ] [[package]] name = "skippy-metrics" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" [[package]] name = "skippy-protocol" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "prost 0.14.4", "prost-build 0.14.4", @@ -9780,8 +9767,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "anyhow", "libc", @@ -9794,8 +9781,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "ahash", "anyhow", @@ -9806,6 +9793,7 @@ dependencies = [ "clap", "futures-util", "libc", + "mesh-llm-events", "mesh-native-serving-plugin-api", "model-artifact", "openai-frontend", @@ -9828,16 +9816,16 @@ dependencies = [ [[package]] name = "skippy-tokenizer" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.76.0-rc4" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc4#8b4cdd727075b86573cf8dcca9e886d2a0b319c4" +version = "0.76.0-rc6" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" dependencies = [ "serde", "serde_json", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 35b04c70c18..efecde70d94 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -111,14 +111,14 @@ buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" } buzz_ws_client_pkg = { package = "buzz-ws-client", path = "../../crates/buzz-ws-client" } portable-pty = "0.9" iroh = { version = "1.0.3", optional = true } -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", 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", tag = "v0.76.0-rc4", 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", tag = "v0.76.0-rc6", 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", tag = "v0.76.0-rc6", 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", tag = "v0.76.0-rc4", package = "mesh-llm-client", optional = true } -mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-node", optional = true } -mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-system", optional = true } -mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc4", package = "mesh-llm-events", optional = true } +mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-client", optional = true } +mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-node", optional = true } +mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-system", optional = true } +mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-events", optional = true } base64 = "0.22" sha2 = "0.11" tar = "0.4" From d99cba5bbb7f706418ef0c4b5527b8736d708d99 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 25 Aug 2026 11:16:46 +1000 Subject: [PATCH 06/20] fix(mesh): run the mesh unenforced on relays that do not enforce membership MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mesh admission is seeded from the relay's NIP-43 membership snapshot (kind:13534), which is built purely from the `relay_members` table. On a relay running with `BUZZ_REQUIRE_RELAY_MEMBERSHIP=false`, signing in never writes a `relay_members` row, so that snapshot lists at most whoever was added out of band. Ordinary participants are absent from it. Desktop treated that as an authoritative roster, so every node resolved an empty intersection, fell back to `trust_owners([self])`, and sat at "1 NODE" with an empty peer list forever. Two machines sharing compute on the same open relay could never see each other, through any number of restarts. Admission was only part of it: `availability_from_events` also required a snapshot and filtered status notes to `members.contains(reporter)`, so serve-target discovery was membership-gated too. Relaxing admission alone would still have left peers undiscoverable. Resolve the relay's membership mode from its NIP-11 document instead. The relay advertises NIP-43 only when membership is actually enforced, so the advertisement is a faithful signal. When it is absent, skip the snapshot requirement for routing and pass `trusted_owner_ids: None`, which the existing SDK path already maps to `TrustPolicy::Off` — stock MeshLLM behaviour, scoped by the relay-derived mesh topic. Membership emptiness deliberately does NOT drive this. On an enforcing relay a missing snapshot is a transient replication gap, and the existing code treats it as an error precisely so a blip cannot flap a live node down to self-only. Keying open-mesh off "no snapshot" would have turned that blip into a silent loss of admission enforcement. A failed NIP-11 probe therefore falls back to the enforcing behaviour: an unreachable relay can never relax admission. Closed-relay behaviour is unchanged, including the fail-closed error and the shrink hysteresis. `roster_reconcile_action` maps `Ok(None)` to `Keep`: a node already enforcing an allowlist keeps it, so a mid-flight NIP-11 change cannot de-restrict a running mesh. Freshness and owner-signed endpoint binding still gate routing on an open relay; only the membership intersection is lifted. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- desktop/src-tauri/src/commands/mesh_llm.rs | 133 +++++++++++++++--- desktop/src-tauri/src/mesh_llm/coordinator.rs | 41 ++++-- desktop/src-tauri/src/mesh_llm/discovery.rs | 88 ++++++++++-- desktop/src-tauri/src/mesh_llm/mod.rs | 3 +- desktop/src-tauri/src/mesh_llm/mod_tests.rs | 127 +++++++++++++++++ 5 files changed, 348 insertions(+), 44 deletions(-) diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 7356cd7fc0c..3789121a526 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -176,9 +176,63 @@ fn advance_mesh_status_cursor( Ok(cursor) } +/// Resolve whether a relay enforces NIP-43 relay membership, from its NIP-11 +/// document. +/// +/// The relay advertises NIP-43 only when membership is actually enforced +/// (`nip11_facts` in `buzz-relay`), so this is the one signal that separates +/// "no roster because nobody is a direct member here" from "no roster because +/// this read was incomplete". A probe failure is reported as `Err` and treated +/// as enforcing by callers: an unreachable relay must never be able to relax +/// admission. +async fn probe_relay_mesh_mode(relay_url: &str) -> Result { + let http_url = relay::relay_http_base_url(relay_url); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|error| format!("failed to build NIP-11 client: {error}"))?; + let document: serde_json::Value = client + .get(&http_url) + .header("Accept", "application/nostr+json") + .send() + .await + .map_err(|error| format!("NIP-11 probe failed: {error}"))? + .json() + .await + .map_err(|error| format!("NIP-11 document was not JSON: {error}"))?; + Ok(mesh_llm::relay_mesh_mode_from_nip11(&document)) +} + async fn query_mesh_discovery_events_at( state: &AppState, relay_url: &str, +) -> Result, String> { + query_mesh_discovery_events_for_mode( + state, + relay_url, + resolved_relay_mesh_mode(relay_url).await, + ) + .await +} + +/// Probe the relay's membership mode, falling back to the enforcing behaviour. +/// +/// A failed probe must not open the mesh up: an unreachable or malformed NIP-11 +/// document leaves admission exactly as strict as it is today. +async fn resolved_relay_mesh_mode(relay_url: &str) -> mesh_llm::MeshRelayMode { + match probe_relay_mesh_mode(relay_url).await { + Ok(mode) => mode, + Err(error) => { + eprintln!("buzz-mesh: NIP-11 probe failed; assuming membership is enforced: {error}"); + mesh_llm::MeshRelayMode::ClosedMembershipEnforced + } + } +} + +async fn query_mesh_discovery_events_for_mode( + state: &AppState, + relay_url: &str, + mode: mesh_llm::MeshRelayMode, ) -> Result, String> { let api_base_url = relay::relay_http_base_url(relay_url); let mut events = @@ -193,13 +247,24 @@ async fn query_mesh_discovery_events_at( // means the query is incomplete: surface it as an error so the // reconcile loop keeps the current allowlist instead of flapping the // node down to self-only on a successful-but-empty response. - if !mesh_llm::has_membership_snapshot(&events) { - return Err("relay returned no membership snapshot".to_string()); + // + // A relay that does not enforce membership never populates + // `relay_members` from sign-in, so there is no roster to wait for and + // no incomplete read to guard against. Fall through and read status + // notes unfiltered so the caller can run the mesh unenforced. + if !mode.is_open() { + if !mesh_llm::has_membership_snapshot(&events) { + return Err("relay returned no membership snapshot".to_string()); + } + return Ok(events); } - return Ok(events); } let mut status_filter = mesh_llm::mesh_status_filter(); - status_filter["authors"] = serde_json::json!(member_pubkeys); + // On an enforcing relay, scope status reads to current members. Without a + // roster there is nobody to scope to, so read every reporter's note. + if !member_pubkeys.is_empty() { + status_filter["authors"] = serde_json::json!(member_pubkeys); + } let mut previous_cursor: Option<(u64, String)> = None; loop { @@ -231,29 +296,39 @@ async fn query_mesh_discovery_events(state: &AppState) -> Result Result, String> { - let events = query_mesh_discovery_events(state).await?; - Ok(mesh_llm::owner_ids_from_events(&events)) +/// `None` means the relay does not enforce membership: there is no roster to +/// intersect and the node must run unenforced (`TrustPolicy::Off`), exactly as +/// stock MeshLLM does. `Some` carries an enforcing relay's roster. +pub(crate) async fn resolve_trusted_owner_ids( + state: &AppState, +) -> Result>, String> { + resolve_trusted_owner_ids_at(state, &relay::relay_ws_url_with_override(state)).await } pub(crate) async fn resolve_trusted_owner_ids_at( state: &AppState, relay_url: &str, -) -> Result, String> { - let events = query_mesh_discovery_events_at(state, relay_url).await?; - Ok(mesh_llm::owner_ids_from_events(&events)) +) -> Result>, String> { + let mode = resolved_relay_mesh_mode(relay_url).await; + let events = query_mesh_discovery_events_for_mode(state, relay_url, mode).await?; + if mode.is_open() { + return Ok(None); + } + Ok(Some(mesh_llm::owner_ids_from_events(&events))) } /// Resolve the roster for an initial node *start*, failing closed to self-only /// (an empty roster) when the relay query fails. This is safe only at start: /// there is no established allowlist to preserve yet. The periodic /// `reconcile_roster` path must NOT use this — it has a live roster to keep. -pub(crate) async fn resolve_trusted_owner_ids_or_self_only(state: &AppState) -> Vec { +pub(crate) async fn resolve_trusted_owner_ids_or_self_only( + state: &AppState, +) -> Option> { match resolve_trusted_owner_ids(state).await { Ok(owners) => owners, Err(error) => { eprintln!("buzz-mesh: roster query failed; allowing only this node: {error}"); - Vec::new() + Some(Vec::new()) } } } @@ -291,12 +366,13 @@ pub(crate) async fn resolve_buzz_mesh_join_targets_at( state: &AppState, relay_url: &str, ) -> Result, String> { - let events = query_mesh_discovery_events_at(state, relay_url).await?; + let mode = resolved_relay_mesh_mode(relay_url).await; + let events = query_mesh_discovery_events_for_mode(state, relay_url, mode).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, + mesh_llm::availability_from_events_for_mode(events, mode).serve_targets, &self_owner_id, )) } @@ -308,15 +384,19 @@ pub(crate) async fn resolve_buzz_mesh_join_targets_at( async fn resolve_buzz_mesh_startup_at( state: &AppState, relay_url: &str, -) -> (Vec, Option) { - match query_mesh_discovery_events_at(state, relay_url).await { +) -> (Option>, Option) { + let mode = resolved_relay_mesh_mode(relay_url).await; + match query_mesh_discovery_events_for_mode(state, relay_url, mode).await { Ok(events) => { - let trusted_owner_ids = mesh_llm::owner_ids_from_events(&events); + // An open relay has no roster to enforce: `None` runs the node + // unenforced instead of collapsing it to a self-only allowlist. + let trusted_owner_ids = + (!mode.is_open()).then(|| 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, + mesh_llm::availability_from_events_for_mode(events, mode).serve_targets, &identity.owner_id, ) .into_iter() @@ -332,7 +412,7 @@ async fn resolve_buzz_mesh_startup_at( eprintln!( "buzz-mesh: startup discovery failed; allowing only this node and starting isolated for now: {error}" ); - (Vec::new(), None) + (Some(Vec::new()), None) } } } @@ -373,7 +453,7 @@ pub(crate) async fn restore_mesh_sharing(app: &AppHandle, state: &AppState) -> C join_token, mesh_name: Some(buzz_mesh_name_for_relay(&relay_url)), relay_url: Some(relay_url), - trusted_owner_ids: Some(trusted_owner_ids), + trusted_owner_ids, }; let started = mesh_llm::DesktopMeshRuntime::start(request) .await @@ -446,10 +526,17 @@ pub async fn mesh_start_node( // 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 roster_unresolved = request.trusted_owner_ids.is_none(); + if roster_unresolved || request.join_token.is_none() { let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup_at(&state, &relay_url).await; - request.trusted_owner_ids.get_or_insert(trusted_owner_ids); + // Adopt the resolver's decision only when the caller supplied no + // roster. `None` from the resolver is meaningful (open relay: run + // unenforced), so this cannot use `get_or_insert`, which would treat + // it as "nothing resolved" and leave the field untouched. + if roster_unresolved { + request.trusted_owner_ids = trusted_owner_ids; + } if request.join_token.is_none() { request.join_token = join_token; } @@ -590,7 +677,7 @@ pub(crate) async fn ensure_client_node_for_model( join_token: Some(join_token.clone()), mesh_name: Some(buzz_mesh_name(state)), relay_url: Some(relay::relay_ws_url_with_override(state)), - trusted_owner_ids: Some(resolve_trusted_owner_ids_or_self_only(state).await), + trusted_owner_ids: resolve_trusted_owner_ids_or_self_only(state).await, }; let mut runtime = state.mesh_llm_runtime.lock().await; if let Some(existing) = runtime.as_ref() { diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 066fa463730..1cf07516567 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -234,14 +234,21 @@ fn roster_shrinks(current: &[String], fresh: &[String]) -> bool { /// /// Rules: /// - query failed (`Err`) → `Keep` (never de-admit on a relay blip) +/// - relay enforces no membership (`Ok(None)`) → `Keep` (see below) /// - resolved roster == current → `Keep` (no-op) /// - grows (only additions) → `RestartProcess` immediately (fast admission) /// - shrinks/empties, first observation → `AwaitConfirm` (hold, re-check next poll) /// - shrinks/empties, confirmed → `RestartProcess` (same reduced roster twice) +/// +/// `Ok(None)` means the relay does not enforce NIP-43 membership, so there is +/// no roster to reconcile against. A node that is *already* enforcing an +/// allowlist keeps it: relaxing a live node's admission on the strength of a +/// NIP-11 read would let a relay downgrade (or a spoofed document) de-restrict +/// a running mesh. The open-relay path is chosen at start, not mid-flight. fn roster_reconcile_action( current_owners: &[String], pending_shrink: Option<&[String]>, - query: Result, String>, + query: Result>, String>, ) -> RosterReconcileAction { let fresh = match query { Err(error) => { @@ -250,7 +257,13 @@ fn roster_reconcile_action( ); return RosterReconcileAction::Keep; } - Ok(fresh) => fresh, + Ok(None) => { + eprintln!( + "buzz-mesh: relay does not enforce membership; keeping this node's current allowlist" + ); + return RosterReconcileAction::Keep; + } + Ok(Some(fresh)) => fresh, }; if fresh == current_owners { @@ -561,7 +574,7 @@ mod tests { #[test] fn unchanged_roster_is_a_noop() { let current = vec!["owner-a".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(vec!["owner-a".to_string()])); + let action = roster_reconcile_action(¤t, None, Ok(Some(vec!["owner-a".to_string()]))); assert_eq!(action, RosterReconcileAction::Keep); } @@ -570,7 +583,7 @@ mod tests { fn roster_growth_requests_process_restart_immediately() { let current = vec!["owner-a".to_string()]; let fresh = vec!["owner-a".to_string(), "owner-c".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(fresh)); + let action = roster_reconcile_action(¤t, None, Ok(Some(fresh))); assert_eq!(action, RosterReconcileAction::RestartProcess); } @@ -579,7 +592,7 @@ mod tests { fn roster_shrink_awaits_confirmation_first() { let current = vec!["owner-a".to_string(), "owner-b".to_string()]; let reduced = vec!["owner-a".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(reduced.clone())); + let action = roster_reconcile_action(¤t, None, Ok(Some(reduced.clone()))); assert_eq!(action, RosterReconcileAction::AwaitConfirm(reduced)); } @@ -588,7 +601,7 @@ mod tests { fn roster_shrink_requests_process_restart_once_confirmed() { let current = vec!["owner-a".to_string(), "owner-b".to_string()]; let reduced = vec!["owner-a".to_string()]; - let action = roster_reconcile_action(¤t, Some(&reduced), Ok(reduced.clone())); + let action = roster_reconcile_action(¤t, Some(&reduced), Ok(Some(reduced.clone()))); assert_eq!(action, RosterReconcileAction::RestartProcess); } @@ -599,8 +612,11 @@ mod tests { let current = vec!["a".to_string(), "b".to_string(), "c".to_string()]; let first_reduced = vec!["a".to_string(), "b".to_string()]; let second_reduced = vec!["a".to_string()]; - let action = - roster_reconcile_action(¤t, Some(&first_reduced), Ok(second_reduced.clone())); + let action = roster_reconcile_action( + ¤t, + Some(&first_reduced), + Ok(Some(second_reduced.clone())), + ); assert_eq!(action, RosterReconcileAction::AwaitConfirm(second_reduced)); } @@ -609,10 +625,10 @@ mod tests { #[test] fn genuinely_empty_roster_awaits_then_restarts_to_self_only() { let current = vec!["owner-a".to_string()]; - let first = roster_reconcile_action(¤t, None, Ok(Vec::new())); + let first = roster_reconcile_action(¤t, None, Ok(Some(Vec::new()))); assert_eq!(first, RosterReconcileAction::AwaitConfirm(Vec::new())); let empty: Vec = Vec::new(); - let confirmed = roster_reconcile_action(¤t, Some(&empty), Ok(Vec::new())); + let confirmed = roster_reconcile_action(¤t, Some(&empty), Ok(Some(Vec::new()))); assert_eq!(confirmed, RosterReconcileAction::RestartProcess); } @@ -621,9 +637,10 @@ mod tests { fn roster_shrink_then_recovery_keeps_allowlist() { let current = vec!["owner-a".to_string(), "owner-b".to_string()]; let reduced = vec!["owner-a".to_string()]; - let held = roster_reconcile_action(¤t, None, Ok(reduced.clone())); + let held = roster_reconcile_action(¤t, None, Ok(Some(reduced.clone()))); assert_eq!(held, RosterReconcileAction::AwaitConfirm(reduced.clone())); - let recovered = roster_reconcile_action(¤t, Some(&reduced), Ok(current.clone())); + let recovered = + roster_reconcile_action(¤t, Some(&reduced), Ok(Some(current.clone()))); assert_eq!(recovered, RosterReconcileAction::Keep); } diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index 2df4a7f8c5e..e917229a6d0 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -20,6 +20,53 @@ fn status_is_fresh(event: &nostr::Event, now: u64) -> bool { >= now } +/// Whether the relay enforces NIP-43 relay membership. +/// +/// This is a property of the *relay deployment*, not of any particular query +/// result, and it is resolved from the relay's NIP-11 document (NIP-43 is +/// advertised only when membership is actually enforced). It must never be +/// inferred from an absent membership snapshot: on a closed relay a snapshot +/// can also be missing because of a transient replication gap, and treating +/// that as "open" would silently drop admission enforcement exactly when the +/// relay is least healthy. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MeshRelayMode { + /// Membership is enforced (`BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`). Every + /// participant is a NIP-43 direct member, so the membership snapshot is the + /// trust boundary for both admission and routing. + ClosedMembershipEnforced, + /// Membership is not enforced. Signing in never writes a `relay_members` + /// row, so the snapshot lists at most whoever was added out of band and is + /// structurally useless as a trust boundary. Mesh runs unenforced (stock + /// MeshLLM behaviour) and is scoped by the relay-derived mesh topic. + OpenNoMembership, +} + +impl MeshRelayMode { + pub(crate) fn is_open(self) -> bool { + matches!(self, Self::OpenNoMembership) + } +} + +/// Resolve [`MeshRelayMode`] from a relay's NIP-11 document. +/// +/// `buzz-relay` lists NIP-43 in `supported_nips` only when it has a stable +/// signing key *and* `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, so the advertisement +/// is a faithful proxy for "membership is enforced here". A document that omits +/// it — including a malformed one with no `supported_nips` array — describes a +/// relay where signing in never creates a member row. +pub fn relay_mesh_mode_from_nip11(document: &serde_json::Value) -> MeshRelayMode { + let advertises_nip43 = document + .get("supported_nips") + .and_then(|value| value.as_array()) + .is_some_and(|nips| nips.iter().any(|nip| nip.as_u64() == Some(43))); + if advertises_nip43 { + MeshRelayMode::ClosedMembershipEnforced + } else { + MeshRelayMode::OpenNoMembership + } +} + fn dedupe_targets(targets: Vec) -> Vec { let mut by_endpoint_and_model = BTreeMap::<(String, String), MeshServeTarget>::new(); for target in targets { @@ -173,13 +220,36 @@ fn endpoint_binding_is_valid(event: &nostr::Event, content: &serde_json::Value) } pub fn availability_from_events(events: Vec) -> MeshAvailability { + availability_from_events_for_mode(events, MeshRelayMode::ClosedMembershipEnforced) +} + +/// [`availability_from_events`], scoped to the relay's membership mode. +/// +/// On a membership-enforcing relay the NIP-43 snapshot gates routing: a serve +/// target is selectable only when its status note is signed by a current +/// member. On a relay that does not enforce membership there is no such +/// roster to intersect with — requiring one would make every serving peer +/// permanently undiscoverable — so status notes are accepted on their own +/// merits (fresh, well-formed, and carrying a valid owner-signed endpoint +/// binding). The mesh topic is derived from the relay origin, so this widens +/// discovery to that relay's participants, not to the public internet. +pub fn availability_from_events_for_mode( + events: Vec, + mode: MeshRelayMode, +) -> MeshAvailability { if events.is_empty() { return MeshAvailability::unavailable("Buzz shared compute status is not published yet"); } - let Some(members) = latest_membership_list(&events) else { - return MeshAvailability::unavailable( - "Buzz shared compute is waiting for the current member roster", - ); + let members = match latest_membership_list(&events) { + Some(members) => Some(members), + // An enforcing relay always publishes a snapshot (even a zero-member + // one), so its absence there means an incomplete read: stay closed. + None if !mode.is_open() => { + return MeshAvailability::unavailable( + "Buzz shared compute is waiting for the current member roster", + ); + } + None => None, }; // Status is replaceable per member pubkey, so a query returns multiple @@ -191,12 +261,14 @@ pub fn availability_from_events(events: Vec) -> MeshAvailability { let now = nostr::Timestamp::now().as_secs(); for event in events { - if event.kind.as_u16() as u64 != MESH_STATUS_KIND - || !status_is_fresh(&event, now) - || !members.contains(&event.pubkey.to_hex().to_ascii_lowercase()) - { + if event.kind.as_u16() as u64 != MESH_STATUS_KIND || !status_is_fresh(&event, now) { continue; } + if let Some(members) = members.as_ref() { + if !members.contains(&event.pubkey.to_hex().to_ascii_lowercase()) { + continue; + } + } let Ok(content) = serde_json::from_str::(&event.content) else { continue; }; diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index e206c53886a..e2466b55c9c 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -6,7 +6,8 @@ pub use coordinator::{start_coordinator, MeshCoordinator, KIND_BUZZ_MESH_MEMBER_ mod discovery; pub use discovery::{ - availability_from_events, mesh_status_filter, owner_ids_from_events, relay_membership_filter, + availability_from_events, availability_from_events_for_mode, mesh_status_filter, + owner_ids_from_events, relay_membership_filter, relay_mesh_mode_from_nip11, MeshRelayMode, }; pub(crate) use discovery::{ current_member_pubkeys, has_membership_snapshot, MESH_STATUS_PAGE_SIZE, diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index 557cd040fab..97ae2290e8d 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -665,6 +665,133 @@ fn owner_roster_without_membership_list_fails_closed() { assert!(super::owner_ids_from_events(&events).is_empty()); } +#[test] +fn relay_mesh_mode_reads_nip43_advertisement() { + use super::MeshRelayMode; + + // `buzz-relay` advertises NIP-43 only when membership is enforced, so the + // advertisement is the deployment-mode signal. + assert_eq!( + super::relay_mesh_mode_from_nip11(&json!({"supported_nips": [1, 11, 42, 43]})), + MeshRelayMode::ClosedMembershipEnforced + ); + // The live open relay's actual shape (no 43). + assert_eq!( + super::relay_mesh_mode_from_nip11( + &json!({"supported_nips": [1, 2, 10, 11, 16, 17, 23, 25, 29, 33, 38, 42, 50, 56]}) + ), + MeshRelayMode::OpenNoMembership + ); + // A missing or malformed document describes a relay that cannot be + // enforcing membership, so it is treated as open. Callers that could not + // reach the relay at all keep the enforcing default instead of calling in + // here — an unreachable relay must never relax admission. + assert_eq!( + super::relay_mesh_mode_from_nip11(&json!({})), + MeshRelayMode::OpenNoMembership + ); + assert_eq!( + super::relay_mesh_mode_from_nip11(&json!({"supported_nips": "not-an-array"})), + MeshRelayMode::OpenNoMembership + ); +} + +#[test] +fn open_relay_routes_without_a_membership_snapshot() { + use super::MeshRelayMode; + + // The bug this fixes: on a relay that does not enforce membership, no + // kind:13534 snapshot ever lists ordinary users, so requiring one made + // every serving peer permanently undiscoverable — each node sat at + // "1 NODE" with an empty peer list forever. + let endpoint = test_endpoint_token(); + let events = vec![signed_reporter_target( + &"a".repeat(64), + "model-open", + &endpoint, + )]; + + let closed = super::availability_from_events_for_mode( + events.clone(), + MeshRelayMode::ClosedMembershipEnforced, + ); + assert!( + closed.serve_targets.is_empty(), + "an enforcing relay must still wait for its roster" + ); + + let open = super::availability_from_events_for_mode(events, MeshRelayMode::OpenNoMembership); + assert_eq!(open.serve_targets.len(), 1); + assert_eq!(open.serve_targets[0].endpoint_addr, endpoint); +} + +#[test] +fn closed_relay_without_snapshot_never_silently_opens_up() { + use super::MeshRelayMode; + + // Regression guard for the risk this change introduces. On an enforcing + // relay a missing snapshot is a transient read gap, NOT permission to + // admit everyone: if that ever resolved to the open path, one replication + // blip would drop admission enforcement on a closed relay. + let events = vec![ + signed_reporter_target(&"b".repeat(64), "model-a", &test_endpoint_token()), + signed_reporter_target(&"c".repeat(64), "model-b", &test_endpoint_token()), + ]; + + let availability = + super::availability_from_events_for_mode(events, MeshRelayMode::ClosedMembershipEnforced); + assert!(availability.serve_targets.is_empty()); + assert_eq!( + availability.reason.as_deref(), + Some("Buzz shared compute is waiting for the current member roster") + ); +} + +#[test] +fn open_relay_still_enforces_freshness_and_endpoint_binding() { + use super::MeshRelayMode; + + // Dropping the membership intersection must not drop the other + // protections: a stale note is still unroutable, and an unsigned endpoint + // substitution is still rejected. + let stale = signed_reporter_target_at( + &"d".repeat(64), + "stale-model", + &test_endpoint_token(), + 1_000, + ); + let stale_availability = + super::availability_from_events_for_mode(vec![stale], MeshRelayMode::OpenNoMembership); + assert!( + stale_availability.serve_targets.is_empty(), + "freshness still gates routing on an open relay" + ); + + let signed_endpoint = test_endpoint_token(); + let mut forged = serde_json::from_str::( + &signed_reporter_target(&"e".repeat(64), "model-x", &signed_endpoint).content, + ) + .expect("status content is json"); + // Swap in an endpoint the owner never signed for. Assert the substitution + // is real so this test can never pass vacuously by reusing the same token. + let substituted_endpoint = test_endpoint_token(); + assert_ne!(signed_endpoint, substituted_endpoint); + forged["serveTargets"][0]["endpointAddr"] = json!(substituted_endpoint); + let keys = nostr::Keys::parse(&"e".repeat(64)).unwrap(); + let substituted = super::coordinator::build_status_report_event(forged) + .expect("status builder") + .sign_with_keys(&keys) + .expect("signs"); + let forged_availability = super::availability_from_events_for_mode( + vec![substituted], + MeshRelayMode::OpenNoMembership, + ); + assert!( + forged_availability.serve_targets.is_empty(), + "endpoint binding is still verified on an open relay" + ); +} + #[test] fn serving_usage_extracts_local_and_remote_attempts() { // Captured shape from a live serving node's status payload. The local vs From ceaaced941ee9cceff5f68759aed0951b9eca574 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 25 Aug 2026 11:43:50 +1000 Subject: [PATCH 07/20] fix(mesh): make open-relay mode consistent across discovery, reconcile, and probing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the open-mesh change (d99cba5bb), closing three gaps found in review: 1. A stray membership snapshot on an open relay no longer gates routing. Admission runs TrustPolicy::Off there, but discovery still intersected status notes with any kind:13534 roster that happened to exist (added out of band), and the query layer applied the same authors filter — a hybrid where admission admits everyone while discovery hides non-roster peers. Both branches now check the relay mode: open relays ignore membership entirely and rely on freshness plus owner-signed endpoint binding. 2. Reconcile recovers a node isolated by the startup probe fallback. A transient NIP-11 probe failure at start falls back to the enforcing path and a self-only allowlist; treating Ok(None) as an unconditional Keep then pinned that node at "1 NODE" until an app restart — the original bug, reintroduced by one HTTP blip. A self-only roster admits nobody else, so restarting into open mesh relaxes nothing that protected anyone. An allowlist admitting other owners is still never relaxed mid-flight. 3. Successful NIP-11 probes are memoized per relay URL. The deployment mode does not flap at runtime, and discovery re-probed on every reconcile poll. Failures are not cached, so the fallback in (2) re-probes and recovers. Co-authored-by: Michael Neale Signed-off-by: Michael Neale --- desktop/src-tauri/src/commands/mesh_llm.rs | 31 +++++++++-- desktop/src-tauri/src/mesh_llm/coordinator.rs | 52 +++++++++++++++++-- desktop/src-tauri/src/mesh_llm/discovery.rs | 27 ++++++---- desktop/src-tauri/src/mesh_llm/mod_tests.rs | 39 ++++++++++++++ 4 files changed, 131 insertions(+), 18 deletions(-) diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 3789121a526..8772bf819fb 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -219,9 +219,29 @@ async fn query_mesh_discovery_events_at( /// /// A failed probe must not open the mesh up: an unreachable or malformed NIP-11 /// document leaves admission exactly as strict as it is today. +/// +/// Successful probes are memoized per relay URL: the deployment mode +/// (`BUZZ_REQUIRE_RELAY_MEMBERSHIP`) does not flap at runtime, and the +/// discovery/reconcile paths would otherwise re-issue this HTTP GET on every +/// poll. Failures are NOT cached, so a node that fell back to enforcing on a +/// transient blip re-probes on the next poll and can recover. async fn resolved_relay_mesh_mode(relay_url: &str) -> mesh_llm::MeshRelayMode { + static CACHE: std::sync::OnceLock< + std::sync::Mutex>, + > = std::sync::OnceLock::new(); + let cache = CACHE.get_or_init(Default::default); + if let Ok(guard) = cache.lock() { + if let Some(mode) = guard.get(relay_url).copied() { + return mode; + } + } match probe_relay_mesh_mode(relay_url).await { - Ok(mode) => mode, + Ok(mode) => { + if let Ok(mut guard) = cache.lock() { + guard.insert(relay_url.to_string(), mode); + } + mode + } Err(error) => { eprintln!("buzz-mesh: NIP-11 probe failed; assuming membership is enforced: {error}"); mesh_llm::MeshRelayMode::ClosedMembershipEnforced @@ -260,9 +280,12 @@ async fn query_mesh_discovery_events_for_mode( } } let mut status_filter = mesh_llm::mesh_status_filter(); - // On an enforcing relay, scope status reads to current members. Without a - // roster there is nobody to scope to, so read every reporter's note. - if !member_pubkeys.is_empty() { + // On an enforcing relay, scope status reads to current members. On an open + // relay the snapshot is not a trust boundary even when one exists (added + // out of band) — admission runs unenforced there, so scoping reads to a + // stray roster would hide peers the mesh admits. Read every reporter's + // note and let freshness + endpoint-binding checks gate routing. + if !mode.is_open() && !member_pubkeys.is_empty() { status_filter["authors"] = serde_json::json!(member_pubkeys); } let mut previous_cursor: Option<(u64, String)> = None; diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 1cf07516567..4bf870172b6 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -234,17 +234,24 @@ fn roster_shrinks(current: &[String], fresh: &[String]) -> bool { /// /// Rules: /// - query failed (`Err`) → `Keep` (never de-admit on a relay blip) -/// - relay enforces no membership (`Ok(None)`) → `Keep` (see below) +/// - relay enforces no membership (`Ok(None)`), roster beyond self → `Keep` (see below) +/// - relay enforces no membership (`Ok(None)`), self-only roster → `RestartProcess` (see below) /// - resolved roster == current → `Keep` (no-op) /// - grows (only additions) → `RestartProcess` immediately (fast admission) /// - shrinks/empties, first observation → `AwaitConfirm` (hold, re-check next poll) /// - shrinks/empties, confirmed → `RestartProcess` (same reduced roster twice) /// /// `Ok(None)` means the relay does not enforce NIP-43 membership, so there is -/// no roster to reconcile against. A node that is *already* enforcing an -/// allowlist keeps it: relaxing a live node's admission on the strength of a -/// NIP-11 read would let a relay downgrade (or a spoofed document) de-restrict -/// a running mesh. The open-relay path is chosen at start, not mid-flight. +/// no roster to reconcile against. A node enforcing an allowlist that admits +/// *other* owners keeps it: relaxing a live multi-party allowlist on the +/// strength of a NIP-11 read would let a relay downgrade (or a spoofed +/// document) de-restrict a running mesh. A **self-only** allowlist is +/// different: it admits nobody else, so it protects nothing — and it is +/// exactly the fallback a transient NIP-11 probe failure at startup produces +/// on an open relay. Without recovery here that node stays at "1 NODE" until +/// an app restart (the original bug, reintroduced by a single HTTP blip). +/// Restarting resolves the roster afresh, which on an open relay yields +/// `None` → `TrustPolicy::Off`. fn roster_reconcile_action( current_owners: &[String], pending_shrink: Option<&[String]>, @@ -257,6 +264,13 @@ fn roster_reconcile_action( ); return RosterReconcileAction::Keep; } + Ok(None) if current_owners.is_empty() => { + eprintln!( + "buzz-mesh: relay does not enforce membership and this node is isolated by the \ + startup fallback; restarting to run the mesh unenforced" + ); + return RosterReconcileAction::RestartProcess; + } Ok(None) => { eprintln!( "buzz-mesh: relay does not enforce membership; keeping this node's current allowlist" @@ -571,6 +585,34 @@ mod tests { ); } + // Regression: a transient NIP-11 probe failure at startup on an OPEN relay + // falls back to the enforcing path and starts the node with a self-only + // allowlist. If reconcile then treated `Ok(None)` as an unconditional + // `Keep`, that node would sit at "1 NODE" until an app restart — the + // original open-relay bug, reintroduced by a single HTTP blip. A self-only + // roster admits nobody else, so restarting to run unenforced relaxes + // nothing that was protecting anyone. + #[test] + fn open_relay_recovers_a_fallback_isolated_node() { + let self_only: Vec = Vec::new(); + let action = roster_reconcile_action(&self_only, None, Ok(None)); + assert_eq!( + action, + RosterReconcileAction::RestartProcess, + "a fallback-isolated node on an open relay must restart into open mesh" + ); + } + + // The other side of that rule: an allowlist that admits OTHER owners is + // never relaxed on the strength of a NIP-11 read. A relay downgrade (or a + // spoofed document) must not de-restrict a running mesh. + #[test] + fn open_relay_report_keeps_a_multi_party_allowlist() { + let current = vec!["owner-a".to_string(), "owner-b".to_string()]; + let action = roster_reconcile_action(¤t, None, Ok(None)); + assert_eq!(action, RosterReconcileAction::Keep); + } + #[test] fn unchanged_roster_is_a_noop() { let current = vec!["owner-a".to_string()]; diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index e917229a6d0..e226c7f6521 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -240,16 +240,25 @@ pub fn availability_from_events_for_mode( if events.is_empty() { return MeshAvailability::unavailable("Buzz shared compute status is not published yet"); } - let members = match latest_membership_list(&events) { - Some(members) => Some(members), - // An enforcing relay always publishes a snapshot (even a zero-member - // one), so its absence there means an incomplete read: stay closed. - None if !mode.is_open() => { - return MeshAvailability::unavailable( - "Buzz shared compute is waiting for the current member roster", - ); + // On an open relay the snapshot is not a trust boundary even when one + // happens to exist (added out of band): admission runs `TrustPolicy::Off` + // there, so intersecting routing with a stray roster would hide peers the + // mesh happily admits. Ignore membership entirely and accept status notes + // on their own merits. + let members = if mode.is_open() { + None + } else { + match latest_membership_list(&events) { + Some(members) => Some(members), + // An enforcing relay always publishes a snapshot (even a + // zero-member one), so its absence means an incomplete read: + // stay closed. + None => { + return MeshAvailability::unavailable( + "Buzz shared compute is waiting for the current member roster", + ); + } } - None => None, }; // Status is replaceable per member pubkey, so a query returns multiple diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index 97ae2290e8d..e28569c5fd9 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -725,6 +725,45 @@ fn open_relay_routes_without_a_membership_snapshot() { assert_eq!(open.serve_targets[0].endpoint_addr, endpoint); } +#[test] +fn open_relay_ignores_a_stray_membership_snapshot() { + use super::MeshRelayMode; + + // On an open relay a kind:13534 snapshot can still exist (members added + // out of band), but it is not a trust boundary there: admission runs + // `TrustPolicy::Off` regardless. Intersecting routing with that stray + // roster would hide peers the mesh happily admits — a hybrid state where + // admission and discovery disagree. Routing must ignore it entirely. + let reporter_secret = "a".repeat(64); + let reporter_pubkey = nostr::Keys::parse(&reporter_secret) + .unwrap() + .public_key() + .to_hex(); + let endpoint = test_endpoint_token(); + let events = vec![ + // Snapshot deliberately lists someone else, NOT the reporter. + signed_membership_event(&["f".repeat(64)]), + signed_reporter_target(&reporter_secret, "model-open", &endpoint), + ]; + + let closed = super::availability_from_events_for_mode( + events.clone(), + MeshRelayMode::ClosedMembershipEnforced, + ); + assert!( + closed.serve_targets.is_empty(), + "an enforcing relay still intersects routing with the roster" + ); + + let open = super::availability_from_events_for_mode(events, MeshRelayMode::OpenNoMembership); + assert_eq!( + open.serve_targets.len(), + 1, + "an open relay accepts a non-roster reporter's status: {reporter_pubkey} was hidden" + ); + assert_eq!(open.serve_targets[0].endpoint_addr, endpoint); +} + #[test] fn closed_relay_without_snapshot_never_silently_opens_up() { use super::MeshRelayMode; From fa252b56c279b377e73d70ce673ad94f5c286c14 Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Tue, 25 Aug 2026 12:42:43 +1000 Subject: [PATCH 08/20] feat(mesh): move mesh-llm dependency to 0.76.0-rc7 Bump every MeshLLM git dependency in buzz-relay and buzz-desktop from v0.76.0-rc6 to the released v0.76.0-rc7 tag at 5d07a20b, and regenerate both lockfiles. The transitive lockfile delta follows rc7's dependency graph; json5 is removed and the Windows/proc-macro families move to the versions rc7 selects. Review also closes two open-relay gaps: malformed NIP-11 mode evidence now keeps the fail-closed fallback, and persisted-agent bootstrap target resolution uses the relay mode instead of reapplying closed-membership filtering on open relays. Verification: both package suites compile against rc7. Desktop mesh-focused tests pass (88 passed, 1 ignored). The full desktop suite retains the pre-existing inherited shared-compute assertion failure (mesh vs auto); the relay suite retains the pre-existing mesh demo forwarded-arm HTTP 504 (908 passed, 1 failed, 48 ignored). Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- Cargo.lock | 206 ++++++++---------- crates/buzz-relay/Cargo.toml | 4 +- desktop/src-tauri/Cargo.lock | 224 +++++++++----------- desktop/src-tauri/Cargo.toml | 12 +- desktop/src-tauri/src/commands/mesh_llm.rs | 24 +-- desktop/src-tauri/src/mesh_llm/discovery.rs | 21 +- desktop/src-tauri/src/mesh_llm/mod_tests.rs | 20 +- 7 files changed, 227 insertions(+), 284 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 184ac591f04..2d3c09ed097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,7 +798,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling 0.23.0", + "darling 0.20.11", "ident_case", "prettyplease", "proc-macro2", @@ -3196,7 +3196,7 @@ dependencies = [ "log", "rustversion", "windows-link 0.2.1", - "windows-result 0.3.4", + "windows-result 0.4.1", ] [[package]] @@ -3810,7 +3810,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4385,16 +4385,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" -dependencies = [ - "serde", - "ucd-trie", -] - [[package]] name = "jsonpath-rust" version = "0.7.5" @@ -4856,8 +4846,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "hex", "mesh-llm-client", @@ -4866,8 +4856,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -4877,21 +4867,18 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "mesh-llm-client" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", "bytes", - "crypto_box", - "ed25519-dalek", - "hex", "httparse", "iroh", "mesh-llm-identity", @@ -4901,11 +4888,9 @@ dependencies = [ "model-artifact", "nostr-sdk", "prost 0.14.3", - "rand 0.10.1", "rustls", "serde", "serde_json", - "sha2 0.10.9", "thiserror 2.0.18", "tokio", "tracing", @@ -4914,8 +4899,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -4930,8 +4915,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -4940,8 +4925,8 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "chrono", @@ -4957,8 +4942,8 @@ dependencies = [ [[package]] name = "mesh-llm-gpu-bench" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", "serde_json", @@ -4967,8 +4952,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", "serde_json", @@ -4976,8 +4961,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "mesh-llm-native-runtime", ] @@ -5013,8 +4998,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "argon2", @@ -5036,7 +5021,6 @@ dependencies = [ "http-body-util", "httparse", "iroh", - "json5", "keyring", "libc", "mdns-sd", @@ -5082,7 +5066,6 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_yaml", "sha2 0.10.9", "skippy-coordinator", "skippy-ffi", @@ -5110,8 +5093,8 @@ dependencies = [ [[package]] name = "mesh-llm-identity" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "argon2", "base64 0.22.1", @@ -5132,8 +5115,8 @@ dependencies = [ [[package]] name = "mesh-llm-log-store" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "chrono", "data-encoding", @@ -5150,8 +5133,8 @@ dependencies = [ [[package]] name = "mesh-llm-native-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "serde", @@ -5161,8 +5144,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-types", @@ -5175,8 +5158,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", @@ -5192,8 +5175,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5211,8 +5194,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "hex", @@ -5224,8 +5207,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "hex", "sha2 0.10.9", @@ -5233,16 +5216,17 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "iroh", + "serde_json", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5264,8 +5248,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5279,8 +5263,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5290,8 +5274,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "chrono", @@ -5317,8 +5301,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "hex", "serde", @@ -5328,13 +5312,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "mesh-mixture-of-agents" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5347,13 +5331,13 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "libloading", @@ -5499,8 +5483,8 @@ dependencies = [ [[package]] name = "model-artifact" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", @@ -5510,8 +5494,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", @@ -5528,8 +5512,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "bytes", @@ -5548,16 +5532,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "model-artifact", @@ -6358,8 +6342,8 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" [[package]] name = "openai-frontend" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "async-trait", "axum", @@ -6370,7 +6354,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-stream", "tracing", "uuid", ] @@ -8951,8 +8934,8 @@ checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "skippy-cache" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "blake3", @@ -8961,29 +8944,29 @@ dependencies = [ [[package]] name = "skippy-coordinator" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "libloading", ] [[package]] name = "skippy-metrics" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "skippy-protocol" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "prost 0.14.3", "prost-build 0.14.3", @@ -8994,8 +8977,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "libc", @@ -9008,8 +8991,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "ahash", "anyhow", @@ -9036,23 +9019,22 @@ dependencies = [ "skippy-topology", "socket2", "tokio", - "tokio-stream", "tonic", "uuid", ] [[package]] name = "skippy-tokenizer" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", "serde_json", @@ -11534,8 +11516,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.61.3", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml index d8a8288e1e7..198a6fda40a 100644 --- a/crates/buzz-relay/Cargo.toml +++ b/crates/buzz-relay/Cargo.toml @@ -86,8 +86,8 @@ async-compression = { version = "0.4.42", features = ["tokio", "gzip"] } dev = ["buzz-auth/dev"] [dev-dependencies] -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } -mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } +mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc7", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] } +mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc7", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] } # Relay-driven mesh lifecycle smoke (examples/mesh_relay_lifecycle_smoke.rs): # the relay client for discovery notes and the exact ed25519 the mesh owner # keys use for binding verification. diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 6ac2ee2d1eb..f19a1708454 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -976,7 +976,7 @@ version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -1639,7 +1639,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1877,7 +1877,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -3283,8 +3283,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", - "windows-result 0.3.4", + "windows-link 0.2.1", + "windows-result 0.4.1", ] [[package]] @@ -4031,7 +4031,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.5.3", + "windows-registry 0.6.1", ] [[package]] @@ -4046,7 +4046,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4691,16 +4691,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "json5" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" -dependencies = [ - "serde", - "ucd-trie", -] - [[package]] name = "jsonptr" version = "0.6.3" @@ -5174,8 +5164,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-client" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "hex", "mesh-llm-client", @@ -5184,8 +5174,8 @@ dependencies = [ [[package]] name = "mesh-llm-api-server" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5195,21 +5185,18 @@ dependencies = [ [[package]] name = "mesh-llm-build-info" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "mesh-llm-client" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", "bytes", - "crypto_box", - "ed25519-dalek", - "hex", "httparse", "iroh", "mesh-llm-identity", @@ -5219,11 +5206,9 @@ dependencies = [ "model-artifact", "nostr-sdk", "prost 0.14.4", - "rand 0.10.2", "rustls", "serde", "serde_json", - "sha2 0.10.9", "thiserror 2.0.18", "tokio", "tracing", @@ -5232,8 +5217,8 @@ dependencies = [ [[package]] name = "mesh-llm-config" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5248,8 +5233,8 @@ dependencies = [ [[package]] name = "mesh-llm-embedded-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-host-runtime", @@ -5258,8 +5243,8 @@ dependencies = [ [[package]] name = "mesh-llm-events" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "chrono", @@ -5275,8 +5260,8 @@ dependencies = [ [[package]] name = "mesh-llm-gpu-bench" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", "serde_json", @@ -5285,8 +5270,8 @@ dependencies = [ [[package]] name = "mesh-llm-guardrails" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", "serde_json", @@ -5294,8 +5279,8 @@ dependencies = [ [[package]] name = "mesh-llm-hardware-profile" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "mesh-llm-native-runtime", ] @@ -5331,8 +5316,8 @@ dependencies = [ [[package]] name = "mesh-llm-host-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "argon2", @@ -5354,7 +5339,6 @@ dependencies = [ "http-body-util", "httparse", "iroh", - "json5", "keyring", "libc", "mdns-sd", @@ -5400,7 +5384,6 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_yaml", "sha2 0.10.9", "skippy-coordinator", "skippy-ffi", @@ -5428,8 +5411,8 @@ dependencies = [ [[package]] name = "mesh-llm-identity" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "argon2", "base64 0.22.1", @@ -5450,8 +5433,8 @@ dependencies = [ [[package]] name = "mesh-llm-log-store" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "chrono", "data-encoding", @@ -5468,8 +5451,8 @@ dependencies = [ [[package]] name = "mesh-llm-native-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "serde", @@ -5479,8 +5462,8 @@ dependencies = [ [[package]] name = "mesh-llm-node" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-types", @@ -5493,8 +5476,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", @@ -5510,8 +5493,8 @@ dependencies = [ [[package]] name = "mesh-llm-plugin-manager" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5529,8 +5512,8 @@ dependencies = [ [[package]] name = "mesh-llm-protocol" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "hex", @@ -5542,8 +5525,8 @@ dependencies = [ [[package]] name = "mesh-llm-release-footer" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "hex", "sha2 0.10.9", @@ -5551,16 +5534,17 @@ dependencies = [ [[package]] name = "mesh-llm-routing" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "iroh", + "serde_json", ] [[package]] name = "mesh-llm-runtime-install" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5582,8 +5566,8 @@ dependencies = [ [[package]] name = "mesh-llm-sdk" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "mesh-llm-api-client", @@ -5597,8 +5581,8 @@ dependencies = [ [[package]] name = "mesh-llm-skills" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "dirs", @@ -5608,8 +5592,8 @@ dependencies = [ [[package]] name = "mesh-llm-system" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "chrono", @@ -5635,8 +5619,8 @@ dependencies = [ [[package]] name = "mesh-llm-types" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "hex", "serde", @@ -5646,13 +5630,13 @@ dependencies = [ [[package]] name = "mesh-llm-ui" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "mesh-mixture-of-agents" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "async-trait", "mesh-llm-guardrails", @@ -5665,13 +5649,13 @@ dependencies = [ [[package]] name = "mesh-native-serving-plugin-api" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "mesh-native-serving-plugin-host" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "libloading 0.8.9", @@ -5772,8 +5756,8 @@ dependencies = [ [[package]] name = "model-artifact" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", @@ -5783,8 +5767,8 @@ dependencies = [ [[package]] name = "model-hf" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "async-trait", @@ -5801,8 +5785,8 @@ dependencies = [ [[package]] name = "model-package" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "bytes", @@ -5821,16 +5805,16 @@ dependencies = [ [[package]] name = "model-ref" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", ] [[package]] name = "model-resolver" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "model-artifact", @@ -6549,7 +6533,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", "syn 2.0.118", @@ -6931,8 +6915,8 @@ dependencies = [ [[package]] name = "openai-frontend" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "async-trait", "axum", @@ -6943,7 +6927,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-stream", "tracing", "uuid", ] @@ -7145,7 +7128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -7905,7 +7888,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools", "log", "multimap", @@ -7925,7 +7908,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools", "log", "multimap", @@ -9724,8 +9707,8 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skippy-cache" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "blake3", @@ -9734,29 +9717,29 @@ dependencies = [ [[package]] name = "skippy-coordinator" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "skippy-ffi" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "libloading 0.8.9", ] [[package]] name = "skippy-metrics" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" [[package]] name = "skippy-protocol" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "prost 0.14.4", "prost-build 0.14.4", @@ -9767,8 +9750,8 @@ dependencies = [ [[package]] name = "skippy-runtime" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "anyhow", "libc", @@ -9781,8 +9764,8 @@ dependencies = [ [[package]] name = "skippy-server" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "ahash", "anyhow", @@ -9809,23 +9792,22 @@ dependencies = [ "skippy-topology", "socket2", "tokio", - "tokio-stream", "tonic", "uuid", ] [[package]] name = "skippy-tokenizer" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", ] [[package]] name = "skippy-topology" -version = "0.76.0-rc6" -source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc6#a8ddb45581cd8ac9f7e58b79365095d6dd8b5a66" +version = "0.76.0-rc7" +source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23" dependencies = [ "serde", "serde_json", @@ -12568,7 +12550,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -13206,8 +13188,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.61.3", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 0497b997e6a..f14375f8171 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -111,14 +111,14 @@ buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" } buzz_ws_client_pkg = { package = "buzz-ws-client", path = "../../crates/buzz-ws-client" } portable-pty = "0.9" iroh = { version = "1.0.3", optional = true } -mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", 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", tag = "v0.76.0-rc6", 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", tag = "v0.76.0-rc7", 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", tag = "v0.76.0-rc7", 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", tag = "v0.76.0-rc6", package = "mesh-llm-client", optional = true } -mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-node", optional = true } -mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-system", optional = true } -mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc6", package = "mesh-llm-events", optional = true } +mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc7", package = "mesh-llm-client", optional = true } +mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc7", package = "mesh-llm-node", optional = true } +mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc7", package = "mesh-llm-system", optional = true } +mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.0-rc7", package = "mesh-llm-events", optional = true } base64 = "0.22" sha2 = "0.11" tar = "0.4" diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 8772bf819fb..892ea4bea07 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -200,19 +200,7 @@ async fn probe_relay_mesh_mode(relay_url: &str) -> Result Result, String> { - query_mesh_discovery_events_for_mode( - state, - relay_url, - resolved_relay_mesh_mode(relay_url).await, - ) - .await + mesh_llm::relay_mesh_mode_from_nip11(&document) } /// Probe the relay's membership mode, falling back to the enforcing behaviour. @@ -307,10 +295,6 @@ async fn query_mesh_discovery_events_for_mode( } } -async fn query_mesh_discovery_events(state: &AppState) -> Result, String> { - query_mesh_discovery_events_at(state, &relay::relay_ws_url_with_override(state)).await -} - /// Resolve the admission roster by intersecting member-signed mesh status /// reporters with the current NIP-43 direct-member list. /// @@ -748,9 +732,11 @@ pub(crate) async fn resolve_mesh_bootstrap_target( if model_id.is_empty() { return Ok(None); } - let events = query_mesh_discovery_events(state).await?; + let relay_url = relay::relay_ws_url_with_override(state); + let mode = resolved_relay_mesh_mode(&relay_url).await; + let events = query_mesh_discovery_events_for_mode(state, &relay_url, mode).await?; Ok(pick_serve_target_for_model( - mesh_llm::availability_from_events(events).serve_targets, + mesh_llm::availability_from_events_for_mode(events, mode).serve_targets, model_id, )) } diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index e226c7f6521..173ed4d559d 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -52,18 +52,19 @@ impl MeshRelayMode { /// /// `buzz-relay` lists NIP-43 in `supported_nips` only when it has a stable /// signing key *and* `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, so the advertisement -/// is a faithful proxy for "membership is enforced here". A document that omits -/// it — including a malformed one with no `supported_nips` array — describes a -/// relay where signing in never creates a member row. -pub fn relay_mesh_mode_from_nip11(document: &serde_json::Value) -> MeshRelayMode { - let advertises_nip43 = document +/// is a faithful proxy for "membership is enforced here" when the field is +/// well formed. A missing or malformed `supported_nips` field is rejected so +/// callers keep the fail-closed fallback rather than treating an ambiguous +/// response as evidence that membership is disabled. +pub fn relay_mesh_mode_from_nip11(document: &serde_json::Value) -> Result { + let nips = document .get("supported_nips") - .and_then(|value| value.as_array()) - .is_some_and(|nips| nips.iter().any(|nip| nip.as_u64() == Some(43))); - if advertises_nip43 { - MeshRelayMode::ClosedMembershipEnforced + .and_then(serde_json::Value::as_array) + .ok_or_else(|| "NIP-11 document has no supported_nips array".to_string())?; + if nips.iter().any(|nip| nip.as_u64() == Some(43)) { + Ok(MeshRelayMode::ClosedMembershipEnforced) } else { - MeshRelayMode::OpenNoMembership + Ok(MeshRelayMode::OpenNoMembership) } } diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index e28569c5fd9..ff98f4adc52 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -673,27 +673,19 @@ fn relay_mesh_mode_reads_nip43_advertisement() { // advertisement is the deployment-mode signal. assert_eq!( super::relay_mesh_mode_from_nip11(&json!({"supported_nips": [1, 11, 42, 43]})), - MeshRelayMode::ClosedMembershipEnforced + Ok(MeshRelayMode::ClosedMembershipEnforced) ); // The live open relay's actual shape (no 43). assert_eq!( super::relay_mesh_mode_from_nip11( &json!({"supported_nips": [1, 2, 10, 11, 16, 17, 23, 25, 29, 33, 38, 42, 50, 56]}) ), - MeshRelayMode::OpenNoMembership - ); - // A missing or malformed document describes a relay that cannot be - // enforcing membership, so it is treated as open. Callers that could not - // reach the relay at all keep the enforcing default instead of calling in - // here — an unreachable relay must never relax admission. - assert_eq!( - super::relay_mesh_mode_from_nip11(&json!({})), - MeshRelayMode::OpenNoMembership - ); - assert_eq!( - super::relay_mesh_mode_from_nip11(&json!({"supported_nips": "not-an-array"})), - MeshRelayMode::OpenNoMembership + Ok(MeshRelayMode::OpenNoMembership) ); + // Missing or malformed mode evidence must fail closed at the caller. An + // ambiguous NIP-11 response is not proof that membership is disabled. + assert!(super::relay_mesh_mode_from_nip11(&json!({})).is_err()); + assert!(super::relay_mesh_mode_from_nip11(&json!({"supported_nips": "not-an-array"})).is_err()); } #[test] From 49efb1c6827a8f83053fde50e18eb6f64494ba99 Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Tue, 25 Aug 2026 15:04:36 +1000 Subject: [PATCH 09/20] fix(mesh): restore hardware model ladder Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- desktop/src-tauri/src/mesh_llm/catalog.rs | 71 +++++++++++++++-------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/desktop/src-tauri/src/mesh_llm/catalog.rs b/desktop/src-tauri/src/mesh_llm/catalog.rs index 72da0c170f4..9990b164277 100644 --- a/desktop/src-tauri/src/mesh_llm/catalog.rs +++ b/desktop/src-tauri/src/mesh_llm/catalog.rs @@ -15,27 +15,34 @@ use mesh_llm_system::vram::{format_rated_capacity, rated_capacity_gb}; /// Buzz-curated tier picks. These are the models we know survive the agent /// harness on shared compute. /// -/// The large pick is Qwen3.8 27B dense: mesh-llm 0.76 routes Qwen3.8 -/// identities to the `qwen35` recurrent family (Mesh-LLM/mesh-llm#1283) and -/// ships it in the compiled `MODEL_CATALOG` (#1343), so the entry below is -/// canonicalized from that catalog rather than synthesized. +/// The recommended ladder follows rated unified memory: +/// - below 32 GB: Gemma 4 E4B; +/// - 32 through 64 GB: Qwen3.5 9B; +/// - above 64 GB: Qwen3.8 27B. +/// +/// The Qwen entries are canonicalized from mesh-llm's compiled +/// `MODEL_CATALOG` rather than synthesized. const CURATED_LARGE: &str = "unsloth/Qwen3.8-27B-GGUF:Q4_K_M"; const CURATED_LARGE_ALIAS: &str = "Qwen3.8-27B-Q4_K_M"; +const CURATED_MEDIUM: &str = "unsloth/Qwen3.5-9B-GGUF:Q4_K_M"; +const CURATED_MEDIUM_ALIAS: &str = "Qwen3.5-9B-Vision-Q4_K_M"; const CURATED_SMALL: &str = "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M"; const CURATED_SMALL_ALIAS: &str = "Gemma-4-E4B-it-Q4_K_M"; /// Superseded large pick, still mapped so machines that already selected it /// keep resolving to a model id Mesh's OpenAI ingress accepts. const LEGACY_LARGE: &str = "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M"; const LEGACY_LARGE_ALIAS: &str = "gemma-4-26B-A4B-it-UD-Q4_K_M"; -/// Rated-capacity boundary between the two curated tiers, in GB (marketing -/// capacity — a "64GB" Mac rates as 64 even though usable AI memory is less). -const CURATED_LARGE_MIN_RATED_GB: u64 = 64; +/// Rated-capacity boundary for the balanced Qwen3.5 9B tier. +const CURATED_MEDIUM_MIN_RATED_GB: u64 = 32; +/// Qwen3.8 27B is reserved for rated capacity classes above 64 GB. +const CURATED_LARGE_MIN_RATED_GB: u64 = 80; /// The Buzz-curated recommendation for a machine's rated memory capacity. fn buzz_recommended_model(rated_gb: Option) -> &'static str { match rated_gb { Some(gb) if gb >= CURATED_LARGE_MIN_RATED_GB => CURATED_LARGE, - _ => CURATED_SMALL, + Some(gb) if gb >= CURATED_MEDIUM_MIN_RATED_GB => CURATED_MEDIUM, + Some(_) | None => CURATED_SMALL, } } @@ -44,6 +51,7 @@ fn buzz_recommended_model(rated_gb: Option) -> &'static str { pub(crate) fn canonical_curated_model_id(model_id: &str) -> &str { match model_id.trim() { CURATED_SMALL_ALIAS => CURATED_SMALL, + CURATED_MEDIUM_ALIAS => CURATED_MEDIUM, CURATED_LARGE_ALIAS => CURATED_LARGE, LEGACY_LARGE_ALIAS => LEGACY_LARGE, other => other, @@ -176,10 +184,11 @@ fn build_catalog( let recommended = Some(buzz_recommended_model(rated_capacity_gb(vram_bytes)).to_string()); for entry in &mut entries { entry.recommended = recommended.as_deref() == Some(entry.name.as_str()); - // Both curated tiers are always offered: the recommended one for this - // machine plus the other pick (e.g. the small one as an explicit - // lighter choice on big machines). - entry.curated = entry.name == CURATED_LARGE || entry.name == CURATED_SMALL; + // All curated tiers are always offered: the recommendation plus + // lighter and heavier alternatives appropriate to other machine tiers. + entry.curated = entry.name == CURATED_LARGE + || entry.name == CURATED_MEDIUM + || entry.name == CURATED_SMALL; } entries.sort_by(|a, b| { @@ -252,20 +261,28 @@ mod tests { } #[test] - fn recommendation_follows_buzz_curated_tiers() { + fn recommendation_follows_buzz_curated_ladder() { assert_eq!(CURATED_SMALL, "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M"); + assert_eq!(CURATED_MEDIUM, "unsloth/Qwen3.5-9B-GGUF:Q4_K_M"); assert_eq!(CURATED_LARGE, "unsloth/Qwen3.8-27B-GGUF:Q4_K_M"); - // 64GB+ rated machines get the large curated pick. - let large = build_catalog(None, 64_000_000_000, 64.0, &[]); + + // Qwen3.8 is reserved for rated capacity classes above 64 GB. + let large = build_catalog(None, 80_000_000_000, 80.0, &[]); assert_eq!(large.recommended.as_deref(), Some(CURATED_LARGE)); let big = build_catalog(None, 128_000_000_000, 128.0, &[]); assert_eq!(big.recommended.as_deref(), Some(CURATED_LARGE)); - // Below the boundary: the small curated pick — never a reasoning - // model, never sub-4B guesswork. - let small = build_catalog(None, 32_000_000_000, 32.0, &[]); + + // The balanced Qwen3.5 tier spans 32 through 64 GB. + let medium = build_catalog(None, 32_000_000_000, 32.0, &[]); + assert_eq!(medium.recommended.as_deref(), Some(CURATED_MEDIUM)); + let medium_max = build_catalog(None, 64_000_000_000, 64.0, &[]); + assert_eq!(medium_max.recommended.as_deref(), Some(CURATED_MEDIUM)); + + // Smaller and unknown machines use the light Gemma tier. + let small = build_catalog(None, 24_000_000_000, 24.0, &[]); assert_eq!(small.recommended.as_deref(), Some(CURATED_SMALL)); - let tiny = build_catalog(None, 16_000_000_000, 16.0, &[]); - assert_eq!(tiny.recommended.as_deref(), Some(CURATED_SMALL)); + let unknown = build_catalog(None, 0, 0.0, &[]); + assert_eq!(unknown.recommended.as_deref(), Some(CURATED_SMALL)); } #[test] @@ -274,6 +291,10 @@ mod tests { canonical_curated_model_id(CURATED_SMALL_ALIAS), CURATED_SMALL ); + assert_eq!( + canonical_curated_model_id(CURATED_MEDIUM_ALIAS), + CURATED_MEDIUM + ); assert_eq!( canonical_curated_model_id(CURATED_LARGE_ALIAS), CURATED_LARGE @@ -288,13 +309,15 @@ mod tests { #[test] fn curated_picks_lead_the_catalog() { let catalog = build_catalog(None, 96_000_000_000, 96.0, &[]); - // Recommended curated entry first, the other curated pick second, - // advanced entries after. + // Recommended curated entry first, then the other two curated tiers, + // with advanced entries after them. assert_eq!(catalog.entries[0].name, CURATED_LARGE); assert!(catalog.entries[0].recommended && catalog.entries[0].curated); - assert_eq!(catalog.entries[1].name, CURATED_SMALL); + assert_eq!(catalog.entries[1].name, CURATED_MEDIUM); assert!(catalog.entries[1].curated && !catalog.entries[1].recommended); - assert!(catalog.entries[2..].iter().all(|e| !e.curated)); + assert_eq!(catalog.entries[2].name, CURATED_SMALL); + assert!(catalog.entries[2].curated && !catalog.entries[2].recommended); + assert!(catalog.entries[3..].iter().all(|e| !e.curated)); // The large pick comes from the compiled catalog with a real size, so // fit ranking is meaningful rather than a placeholder. assert!(catalog.entries[0].size_gb > 10.0); From 109e553b129a210eff4d3a3ab78db923f074fd7a Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Wed, 26 Aug 2026 13:53:22 +1000 Subject: [PATCH 10/20] fix(mesh): classify relay mode in model discovery Route agent-form shared-compute discovery through the same NIP-11 mode resolution used by mesh startup and routing. Open relays no longer trigger the closed-relay member-roster warning. Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- desktop/src-tauri/src/commands/agent_models.rs | 13 +++---------- desktop/src-tauri/src/commands/mesh_llm.rs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index cb809b6c04a..a9f36dc76c7 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -245,16 +245,9 @@ pub async fn discover_agent_models( if input.provider.as_deref().map(str::trim) == Some(crate::managed_agents::RELAY_MESH_PROVIDER_ID) { - let events = crate::relay::query_relay( - &state, - &[ - crate::mesh_llm::mesh_status_filter(), - crate::mesh_llm::relay_membership_filter(), - ], - ) - .await - .map_err(|error| format!("Buzz shared compute model discovery failed: {error}"))?; - let availability = crate::mesh_llm::availability_from_events(events); + let availability = crate::commands::mesh_llm::resolve_mesh_availability(&state) + .await + .map_err(|error| format!("Buzz shared compute model discovery failed: {error}"))?; if availability.models.is_empty() { return Err(availability.reason.unwrap_or_else(|| { "No live Buzz shared compute models are available".to_string() diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 892ea4bea07..fcef63468f5 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -295,6 +295,19 @@ async fn query_mesh_discovery_events_for_mode( } } +/// Resolve model availability using the same relay-mode probe and discovery +/// query as mesh startup and routing. Agent model pickers must go through this +/// path too: the legacy closed-mode helper rejects every open relay because an +/// open relay correctly publishes no membership snapshot. +pub(crate) async fn resolve_mesh_availability( + state: &AppState, +) -> Result { + let relay_url = relay::relay_ws_url_with_override(state); + let mode = resolved_relay_mesh_mode(&relay_url).await; + let events = query_mesh_discovery_events_for_mode(state, &relay_url, mode).await?; + Ok(mesh_llm::availability_from_events_for_mode(events, mode)) +} + /// Resolve the admission roster by intersecting member-signed mesh status /// reporters with the current NIP-43 direct-member list. /// From 96dd6a988c13c5c8bb1be54289408bebf7adf77c Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Thu, 27 Aug 2026 11:45:55 +1000 Subject: [PATCH 11/20] fix(mesh): refresh relay admission mode safely Reject partially malformed supported_nips arrays instead of treating an ambiguous NIP-11 document as evidence that membership is disabled. Remove the permanent relay-mode cache so open runtimes can detect a relay that later enables membership enforcement. Keep established closed runtimes on their existing roster-only reconcile path. Only open and startup-fallback self-only runtimes re-probe mode; failed probes preserve the current policy, while a valid closed result restarts into the fetched allowlist. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- desktop/src-tauri/src/commands/mesh_llm.rs | 76 ++++++++++--------- desktop/src-tauri/src/mesh_llm/coordinator.rs | 70 +++++++++++++++-- desktop/src-tauri/src/mesh_llm/discovery.rs | 14 +++- desktop/src-tauri/src/mesh_llm/mod_tests.rs | 16 +++- 4 files changed, 130 insertions(+), 46 deletions(-) diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index fcef63468f5..a7bc604a773 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -203,38 +203,14 @@ async fn probe_relay_mesh_mode(relay_url: &str) -> Result mesh_llm::MeshRelayMode { - static CACHE: std::sync::OnceLock< - std::sync::Mutex>, - > = std::sync::OnceLock::new(); - let cache = CACHE.get_or_init(Default::default); - if let Ok(guard) = cache.lock() { - if let Some(mode) = guard.get(relay_url).copied() { - return mode; - } - } - match probe_relay_mesh_mode(relay_url).await { - Ok(mode) => { - if let Ok(mut guard) = cache.lock() { - guard.insert(relay_url.to_string(), mode); - } - mode - } - Err(error) => { - eprintln!("buzz-mesh: NIP-11 probe failed; assuming membership is enforced: {error}"); - mesh_llm::MeshRelayMode::ClosedMembershipEnforced - } - } +/// Only a successful, strictly valid NIP-11 document proves a mode. Transport, +/// HTTP, and document errors remain errors so callers can preserve a running +/// admission policy instead of turning an observation failure into a mode +/// transition. +async fn resolved_relay_mesh_mode(relay_url: &str) -> Result { + probe_relay_mesh_mode(relay_url).await } async fn query_mesh_discovery_events_for_mode( @@ -303,7 +279,7 @@ pub(crate) async fn resolve_mesh_availability( state: &AppState, ) -> Result { let relay_url = relay::relay_ws_url_with_override(state); - let mode = resolved_relay_mesh_mode(&relay_url).await; + let mode = resolved_relay_mesh_mode(&relay_url).await?; let events = query_mesh_discovery_events_for_mode(state, &relay_url, mode).await?; Ok(mesh_llm::availability_from_events_for_mode(events, mode)) } @@ -329,7 +305,7 @@ pub(crate) async fn resolve_trusted_owner_ids_at( state: &AppState, relay_url: &str, ) -> Result>, String> { - let mode = resolved_relay_mesh_mode(relay_url).await; + let mode = resolved_relay_mesh_mode(relay_url).await?; let events = query_mesh_discovery_events_for_mode(state, relay_url, mode).await?; if mode.is_open() { return Ok(None); @@ -337,6 +313,24 @@ pub(crate) async fn resolve_trusted_owner_ids_at( Ok(Some(mesh_llm::owner_ids_from_events(&events))) } +/// Refresh the roster of a runtime that is already membership-enforcing. +/// +/// Its admission mode is established, so reconciliation reads only the +/// authoritative membership/status snapshot. NIP-11 timeouts or changes cannot +/// alter this closed-runtime path. +pub(crate) async fn resolve_closed_trusted_owner_ids_at( + state: &AppState, + relay_url: &str, +) -> Result, String> { + let events = query_mesh_discovery_events_for_mode( + state, + relay_url, + mesh_llm::MeshRelayMode::ClosedMembershipEnforced, + ) + .await?; + Ok(mesh_llm::owner_ids_from_events(&events)) +} + /// Resolve the roster for an initial node *start*, failing closed to self-only /// (an empty roster) when the relay query fails. This is safe only at start: /// there is no established allowlist to preserve yet. The periodic @@ -386,7 +380,7 @@ pub(crate) async fn resolve_buzz_mesh_join_targets_at( state: &AppState, relay_url: &str, ) -> Result, String> { - let mode = resolved_relay_mesh_mode(relay_url).await; + let mode = resolved_relay_mesh_mode(relay_url).await?; let events = query_mesh_discovery_events_for_mode(state, relay_url, mode).await?; let self_owner_id = mesh_llm::ensure_owner_identity() .map_err(|error| format!("failed to load mesh owner identity: {error}"))? @@ -405,7 +399,17 @@ async fn resolve_buzz_mesh_startup_at( state: &AppState, relay_url: &str, ) -> (Option>, Option) { - let mode = resolved_relay_mesh_mode(relay_url).await; + let mode = match resolved_relay_mesh_mode(relay_url).await { + Ok(mode) => mode, + Err(error) => { + // Initial startup has no established policy to preserve, so + // ambiguous mode evidence starts isolated and retries later. + eprintln!( + "buzz-mesh: startup NIP-11 probe failed; allowing only this node and starting isolated for now: {error}" + ); + return (Some(Vec::new()), None); + } + }; match query_mesh_discovery_events_for_mode(state, relay_url, mode).await { Ok(events) => { // An open relay has no roster to enforce: `None` runs the node @@ -746,7 +750,7 @@ pub(crate) async fn resolve_mesh_bootstrap_target( return Ok(None); } let relay_url = relay::relay_ws_url_with_override(state); - let mode = resolved_relay_mesh_mode(&relay_url).await; + let mode = resolved_relay_mesh_mode(&relay_url).await?; let events = query_mesh_discovery_events_for_mode(state, &relay_url, mode).await?; Ok(pick_serve_target_for_model( mesh_llm::availability_from_events_for_mode(events, mode).serve_targets, diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 4bf870172b6..5b525e7486c 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -297,6 +297,14 @@ fn roster_reconcile_action( } } +fn runtime_needs_mode_probe(current_owners: Option<&[String]>) -> bool { + current_owners.is_none_or(<[String]>::is_empty) +} + +fn open_runtime_should_restart(query: &Result>, String>) -> bool { + matches!(query, Ok(Some(_))) +} + async fn reconcile_roster( app: &AppHandle, pending_shrink: &mut Option>, @@ -312,21 +320,49 @@ async fn reconcile_roster( } } }; - let Some(current_owners) = current_request.trusted_owner_ids.as_ref() else { + let current_owners = current_request.trusted_owner_ids.as_ref(); + // Closed runtimes keep the existing roster-only reconciliation path. Open + // runtimes re-probe NIP-11 so a later open -> closed relay restart is not + // hidden by a permanent mode cache. + let relay_url = current_request + .relay_url + .as_deref() + .map(str::to_owned) + .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); + if current_owners.is_none() { + let query = + crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await; + if open_runtime_should_restart(&query) { + eprintln!( + "buzz-mesh: relay now enforces membership; restarting Buzz to apply its allowlist" + ); + app.request_restart(); + } else if let Err(error) = query { + eprintln!( + "buzz-mesh: open-relay mode recheck failed; keeping the running policy: {error}" + ); + } *pending_shrink = None; return Ok(()); + } + let Some(current_owners) = current_owners else { + return Ok(()); }; // A failed roster query must NOT be treated as "the roster became empty": // doing so would restart the node down to self-only and de-admit every // 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 relay_url = current_request - .relay_url - .as_deref() - .map(str::to_owned) - .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); - let query = crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await; + let query = if runtime_needs_mode_probe(Some(current_owners)) { + // Self-only is the fail-safe startup policy when the initial NIP-11 + // probe is inconclusive. Re-probe this isolated state so a later valid + // open result can recover it; errors still keep it self-only. + crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await + } else { + crate::commands::mesh_llm::resolve_closed_trusted_owner_ids_at(&state, &relay_url) + .await + .map(Some) + }; match roster_reconcile_action(current_owners, pending_shrink.as_deref(), query) { RosterReconcileAction::Keep => { *pending_shrink = None; @@ -686,6 +722,26 @@ mod tests { assert_eq!(recovered, RosterReconcileAction::Keep); } + #[test] + fn only_open_or_self_only_runtimes_reprobe_mode() { + let self_only: Vec = Vec::new(); + let closed = vec!["owner-a".to_string()]; + assert!(runtime_needs_mode_probe(None)); + assert!(runtime_needs_mode_probe(Some(&self_only))); + assert!(!runtime_needs_mode_probe(Some(&closed))); + } + + #[test] + fn open_runtime_restarts_only_for_a_proven_closed_roster() { + assert!(open_runtime_should_restart(&Ok(Some(vec![ + "owner-a".to_string() + ])))); + assert!(!open_runtime_should_restart(&Ok(None))); + assert!(!open_runtime_should_restart(&Err( + "NIP-11 timed out".to_string() + ))); + } + #[test] fn member_heartbeat_leaves_room_before_admission_status_expires() { assert!( diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index 173ed4d559d..ad6db721a93 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -61,7 +61,19 @@ pub fn relay_mesh_mode_from_nip11(document: &serde_json::Value) -> Result, _>>()?; + if nips.contains(&43) { Ok(MeshRelayMode::ClosedMembershipEnforced) } else { Ok(MeshRelayMode::OpenNoMembership) diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index ff98f4adc52..a1db42225f3 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -682,10 +682,22 @@ fn relay_mesh_mode_reads_nip43_advertisement() { ), Ok(MeshRelayMode::OpenNoMembership) ); - // Missing or malformed mode evidence must fail closed at the caller. An - // ambiguous NIP-11 response is not proof that membership is disabled. + // Missing, malformed, or partially malformed mode evidence must fail at + // the caller. An ambiguous NIP-11 response is not proof of either mode. assert!(super::relay_mesh_mode_from_nip11(&json!({})).is_err()); assert!(super::relay_mesh_mode_from_nip11(&json!({"supported_nips": "not-an-array"})).is_err()); + for malformed in [ + json!({"supported_nips": [1, "43"]}), + json!({"supported_nips": [1, {"nip": 43}]}), + json!({"supported_nips": [1, -1]}), + json!({"supported_nips": [1, 43.5]}), + json!({"supported_nips": [1, 4_294_967_296_u64]}), + ] { + assert!( + super::relay_mesh_mode_from_nip11(&malformed).is_err(), + "partially malformed supported_nips must not prove a relay mode: {malformed}" + ); + } } #[test] From 3e78b290ec2ec2ddb674121722c2ba309f8b2afa Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Thu, 27 Aug 2026 11:54:01 +1000 Subject: [PATCH 12/20] fix(mesh): keep closed availability helper test-only The mode-aware resolver replaced the legacy closed-only helper in production. Keep the compatibility wrapper available to its existing closed-mode tests without leaving an unused production export. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- desktop/src-tauri/src/mesh_llm/discovery.rs | 3 ++- desktop/src-tauri/src/mesh_llm/mod.rs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index ad6db721a93..d347a16d806 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -232,11 +232,12 @@ fn endpoint_binding_is_valid(event: &nostr::Event, content: &serde_json::Value) .is_ok() } +#[cfg(test)] pub fn availability_from_events(events: Vec) -> MeshAvailability { availability_from_events_for_mode(events, MeshRelayMode::ClosedMembershipEnforced) } -/// [`availability_from_events`], scoped to the relay's membership mode. +/// Resolve availability from relay events, scoped to the relay's membership mode. /// /// On a membership-enforcing relay the NIP-43 snapshot gates routing: a serve /// target is selectable only when its status note is signed by a current diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index e2466b55c9c..d6a2b925dba 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -5,9 +5,11 @@ pub(crate) use coordinator::{publish_current_status_once, publish_stopped_status pub use coordinator::{start_coordinator, MeshCoordinator, KIND_BUZZ_MESH_MEMBER_STATUS}; mod discovery; +#[cfg(test)] +pub use discovery::availability_from_events; pub use discovery::{ - availability_from_events, availability_from_events_for_mode, mesh_status_filter, - owner_ids_from_events, relay_membership_filter, relay_mesh_mode_from_nip11, MeshRelayMode, + availability_from_events_for_mode, mesh_status_filter, owner_ids_from_events, + relay_membership_filter, relay_mesh_mode_from_nip11, MeshRelayMode, }; pub(crate) use discovery::{ current_member_pubkeys, has_membership_snapshot, MESH_STATUS_PAGE_SIZE, From 34e31f7e0463154eefd42202e13cc8c14eac12a4 Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Thu, 27 Aug 2026 13:08:03 +1000 Subject: [PATCH 13/20] test(mesh): prove relay mode probes are refreshed Exercise the real NIP-11 HTTP probe three times at one URL as its response changes from open to closed to malformed. This guards against restoring the permanent mode cache and proves malformed evidence cannot be interpreted as open. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- .../src-tauri/src/commands/mesh_llm_tests.rs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/desktop/src-tauri/src/commands/mesh_llm_tests.rs b/desktop/src-tauri/src/commands/mesh_llm_tests.rs index c4e1ae2e425..ad6a8d44d12 100644 --- a/desktop/src-tauri/src/commands/mesh_llm_tests.rs +++ b/desktop/src-tauri/src/commands/mesh_llm_tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::app_state::build_app_state; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; fn target(model_id: &str, endpoint_addr: &str) -> mesh_llm::MeshServeTarget { mesh_llm::MeshServeTarget { @@ -27,6 +28,51 @@ fn reported_target( target } +#[tokio::test] +async fn relay_mode_probe_rechecks_the_same_url_without_caching() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test NIP-11 server"); + let addr = listener.local_addr().expect("test server address"); + let server = tokio::spawn(async move { + for body in [ + r#"{"supported_nips":[1,11,42]}"#, + r#"{"supported_nips":[1,11,42,43]}"#, + r#"{"supported_nips":[1,"43"]}"#, + ] { + let (mut stream, _) = listener.accept().await.expect("accept NIP-11 request"); + let mut request = vec![0; 2048]; + let bytes_read = stream.read(&mut request).await.expect("read request"); + let request = String::from_utf8_lossy(&request[..bytes_read]); + assert!(request.starts_with("GET / HTTP/1.1")); + assert!(request + .to_ascii_lowercase() + .contains("accept: application/nostr+json")); + + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/nostr+json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream + .write_all(response.as_bytes()) + .await + .expect("write response"); + } + }); + let relay_url = format!("ws://{addr}"); + + assert_eq!( + probe_relay_mesh_mode(&relay_url).await, + Ok(mesh_llm::MeshRelayMode::OpenNoMembership) + ); + assert_eq!( + probe_relay_mesh_mode(&relay_url).await, + Ok(mesh_llm::MeshRelayMode::ClosedMembershipEnforced) + ); + assert!(probe_relay_mesh_mode(&relay_url).await.is_err()); + server.await.expect("NIP-11 server task"); +} + #[test] fn buzz_mesh_join_uses_the_same_live_member_from_every_other_node() { let targets = vec![ From 0f1798e207bf9d16107cb35d4ab648dd683ccc6e Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Fri, 28 Aug 2026 09:44:44 +1000 Subject: [PATCH 14/20] fix(mesh): fail closed after proven relay tightening Keep relay-mode evidence distinct from roster availability so an open runtime restarts once NIP-11 proves membership enforcement, even when the first roster snapshot is unavailable. Run the feature-enabled desktop mesh tests in CI and correct the newly exercised wire-model expectation. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- .github/workflows/ci.yml | 4 + desktop/src-tauri/src/commands/mesh_llm.rs | 4 +- .../src/managed_agents/global_config/tests.rs | 2 +- desktop/src-tauri/src/mesh_llm/coordinator.rs | 97 +++++++++++++++---- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81b439a73d1..e35653ff8d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,6 +214,10 @@ jobs: run: just desktop-tauri-test env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Desktop Mesh admission tests + run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm --lib + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" - name: Desktop Tauri compiled-flag verification run: just desktop-tauri-test-compiled-flags env: diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index a7bc604a773..447a798ad6b 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -209,7 +209,9 @@ async fn probe_relay_mesh_mode(relay_url: &str) -> Result Result { +pub(crate) async fn resolved_relay_mesh_mode( + relay_url: &str, +) -> Result { probe_relay_mesh_mode(relay_url).await } diff --git a/desktop/src-tauri/src/managed_agents/global_config/tests.rs b/desktop/src-tauri/src/managed_agents/global_config/tests.rs index 9d090787c7f..97ae18cc52a 100644 --- a/desktop/src-tauri/src/managed_agents/global_config/tests.rs +++ b/desktop/src-tauri/src/managed_agents/global_config/tests.rs @@ -507,7 +507,7 @@ fn inherited_shared_compute_translates_to_supported_agent_transport() { ); assert_eq!( effective.env.get("BUZZ_AGENT_MODEL").map(String::as_str), - Some("auto") + Some(super::super::RELAY_MESH_VIRTUAL_MODEL_ID) ); assert_eq!( effective diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 5b525e7486c..92693974cf6 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -301,8 +301,46 @@ fn runtime_needs_mode_probe(current_owners: Option<&[String]>) -> bool { current_owners.is_none_or(<[String]>::is_empty) } -fn open_runtime_should_restart(query: &Result>, String>) -> bool { - matches!(query, Ok(Some(_))) +#[derive(Debug, PartialEq, Eq)] +enum OpenRuntimeObservation { + Open, + ClosedWithRoster, + ClosedWithoutRoster(String), + Unknown(String), +} + +fn open_runtime_should_restart(observation: &OpenRuntimeObservation) -> bool { + matches!( + observation, + OpenRuntimeObservation::ClosedWithRoster | OpenRuntimeObservation::ClosedWithoutRoster(_) + ) +} + +fn classify_open_runtime_observation( + mode: Result, + closed_roster: Option, String>>, +) -> OpenRuntimeObservation { + match mode { + Ok(mode) if mode.is_open() => OpenRuntimeObservation::Open, + Ok(_) => match closed_roster { + Some(Ok(_)) => OpenRuntimeObservation::ClosedWithRoster, + Some(Err(error)) => OpenRuntimeObservation::ClosedWithoutRoster(error), + None => OpenRuntimeObservation::ClosedWithoutRoster( + "closed relay roster was not queried".to_string(), + ), + }, + Err(error) => OpenRuntimeObservation::Unknown(error), + } +} + +async fn observe_open_runtime_relay(state: &AppState, relay_url: &str) -> OpenRuntimeObservation { + let mode = crate::commands::mesh_llm::resolved_relay_mesh_mode(relay_url).await; + let closed_roster = if matches!(&mode, Ok(mode) if !mode.is_open()) { + Some(crate::commands::mesh_llm::resolve_closed_trusted_owner_ids_at(state, relay_url).await) + } else { + None + }; + classify_open_runtime_observation(mode, closed_roster) } async fn reconcile_roster( @@ -330,14 +368,19 @@ async fn reconcile_roster( .map(str::to_owned) .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); if current_owners.is_none() { - let query = - crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await; - if open_runtime_should_restart(&query) { - eprintln!( - "buzz-mesh: relay now enforces membership; restarting Buzz to apply its allowlist" - ); + let observation = observe_open_runtime_relay(&state, &relay_url).await; + if open_runtime_should_restart(&observation) { + match &observation { + OpenRuntimeObservation::ClosedWithRoster => eprintln!( + "buzz-mesh: relay now enforces membership; restarting Buzz to apply its allowlist" + ), + OpenRuntimeObservation::ClosedWithoutRoster(error) => eprintln!( + "buzz-mesh: relay now enforces membership but its roster is unavailable; restarting Buzz fail-closed: {error}" + ), + OpenRuntimeObservation::Open | OpenRuntimeObservation::Unknown(_) => {} + } app.request_restart(); - } else if let Err(error) = query { + } else if let OpenRuntimeObservation::Unknown(error) = observation { eprintln!( "buzz-mesh: open-relay mode recheck failed; keeping the running policy: {error}" ); @@ -732,14 +775,34 @@ mod tests { } #[test] - fn open_runtime_restarts_only_for_a_proven_closed_roster() { - assert!(open_runtime_should_restart(&Ok(Some(vec![ - "owner-a".to_string() - ])))); - assert!(!open_runtime_should_restart(&Ok(None))); - assert!(!open_runtime_should_restart(&Err( - "NIP-11 timed out".to_string() - ))); + fn open_to_closed_with_unavailable_roster_fails_closed() { + let observation = classify_open_runtime_observation( + Ok(crate::mesh_llm::MeshRelayMode::ClosedMembershipEnforced), + Some(Err("relay returned no membership snapshot".to_string())), + ); + assert_eq!( + observation, + OpenRuntimeObservation::ClosedWithoutRoster( + "relay returned no membership snapshot".to_string() + ) + ); + assert!(open_runtime_should_restart(&observation)); + } + + #[test] + fn open_runtime_restarts_for_any_proven_closed_mode() { + assert!(!open_runtime_should_restart(&OpenRuntimeObservation::Open)); + assert!(open_runtime_should_restart( + &OpenRuntimeObservation::ClosedWithRoster + )); + assert!(open_runtime_should_restart( + &OpenRuntimeObservation::ClosedWithoutRoster( + "relay returned no membership snapshot".to_string() + ) + )); + assert!(!open_runtime_should_restart( + &OpenRuntimeObservation::Unknown("NIP-11 timed out".to_string()) + )); } #[test] From 358ca97b8875b020f145fe02f692251374424efa Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Sat, 29 Aug 2026 08:06:47 +1000 Subject: [PATCH 15/20] fix(mesh): reject unsuccessful NIP-11 responses Require a successful HTTP status before accepting supported_nips as relay-mode evidence. Cover a valid-shaped open body returned with 503 so an HTTP failure cannot disable admission. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- desktop/src-tauri/src/commands/mesh_llm.rs | 2 ++ .../src-tauri/src/commands/mesh_llm_tests.rs | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 447a798ad6b..1b7620dcf93 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -197,6 +197,8 @@ async fn probe_relay_mesh_mode(relay_url: &str) -> Result Date: Sat, 29 Aug 2026 08:41:20 +1000 Subject: [PATCH 16/20] fix(mesh): bound admission mode transitions Require two consecutive observations before whole-app restarts, fail closed after repeated unknown mode reads, and throttle self-only-to-open recovery restarts across process launches. Add feature-enabled mesh clippy to CI and resolve the warnings it exposes. Clarify transport trust and catalog contracts. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- .github/workflows/ci.yml | 4 + desktop/src-tauri/src/commands/mesh_llm.rs | 4 + desktop/src-tauri/src/commands/workspace.rs | 10 +- .../src-tauri/src/managed_agents/restore.rs | 32 ++-- desktop/src-tauri/src/mesh_llm/catalog.rs | 14 +- desktop/src-tauri/src/mesh_llm/coordinator.rs | 136 +++++++++------ .../src/mesh_llm/coordinator_admission.rs | 163 ++++++++++++++++++ desktop/src-tauri/src/mesh_llm/mod.rs | 1 + 8 files changed, 286 insertions(+), 78 deletions(-) create mode 100644 desktop/src-tauri/src/mesh_llm/coordinator_admission.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00474ce0ff3..87c45a8b4ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,6 +214,10 @@ jobs: run: just desktop-tauri-test env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Desktop Mesh clippy + run: cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --features mesh-llm -- -D warnings + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" - name: Desktop Mesh admission tests run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm --lib env: diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 1b7620dcf93..e5ff5e39d56 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -179,6 +179,10 @@ fn advance_mesh_status_cursor( /// Resolve whether a relay enforces NIP-43 relay membership, from its NIP-11 /// document. /// +/// The relay's advertisement is authoritative only to the extent that the +/// HTTP transport is authenticated: `wss://` relay URLs produce HTTPS NIP-11 +/// reads, while a `ws://` override is vulnerable to on-path modification. +/// /// The relay advertises NIP-43 only when membership is actually enforced /// (`nip11_facts` in `buzz-relay`), so this is the one signal that separates /// "no roster because nobody is a direct member here" from "no roster because diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index 77d519b94ba..128a54753d6 100644 --- a/desktop/src-tauri/src/commands/workspace.rs +++ b/desktop/src-tauri/src/commands/workspace.rs @@ -345,7 +345,7 @@ pub async fn apply_workspace( } } }); - return Ok(()); + Ok(()) } #[cfg(not(feature = "mesh-llm"))] @@ -364,9 +364,11 @@ pub async fn apply_workspace( return Ok(()); } - assert_current_apply_generation(&state.workspace_apply_generation, apply_generation)?; - - Ok(()) + #[cfg(not(feature = "mesh-llm"))] + { + assert_current_apply_generation(&state.workspace_apply_generation, apply_generation)?; + Ok(()) + } } #[cfg(test)] diff --git a/desktop/src-tauri/src/managed_agents/restore.rs b/desktop/src-tauri/src/managed_agents/restore.rs index a225f492d33..1c64cdfb910 100644 --- a/desktop/src-tauri/src/managed_agents/restore.rs +++ b/desktop/src-tauri/src/managed_agents/restore.rs @@ -534,22 +534,6 @@ pub(crate) fn spawn_pending_profile_reconciliations(app: &tauri::AppHandle, work } } -#[cfg(test)] -mod profile_reconcile_tests { - use super::profile_reconcile_completed; - use crate::commands::ProfileReconcileOutcome; - - #[test] - fn skipped_reconciliation_never_retires_pending_work() { - assert!(profile_reconcile_completed( - ProfileReconcileOutcome::Reconciled - )); - assert!(!profile_reconcile_completed( - ProfileReconcileOutcome::SkippedDisabled - )); - } -} - #[cfg(feature = "mesh-llm")] fn persist_restore_error( app: &tauri::AppHandle, @@ -567,3 +551,19 @@ fn persist_restore_error( record.last_error = Some(error); save_managed_agents(app, &records) } + +#[cfg(test)] +mod profile_reconcile_tests { + use super::profile_reconcile_completed; + use crate::commands::ProfileReconcileOutcome; + + #[test] + fn skipped_reconciliation_never_retires_pending_work() { + assert!(profile_reconcile_completed( + ProfileReconcileOutcome::Reconciled + )); + assert!(!profile_reconcile_completed( + ProfileReconcileOutcome::SkippedDisabled + )); + } +} diff --git a/desktop/src-tauri/src/mesh_llm/catalog.rs b/desktop/src-tauri/src/mesh_llm/catalog.rs index 9990b164277..00cd8ea8fa4 100644 --- a/desktop/src-tauri/src/mesh_llm/catalog.rs +++ b/desktop/src-tauri/src/mesh_llm/catalog.rs @@ -17,8 +17,8 @@ use mesh_llm_system::vram::{format_rated_capacity, rated_capacity_gb}; /// /// The recommended ladder follows rated unified memory: /// - below 32 GB: Gemma 4 E4B; -/// - 32 through 64 GB: Qwen3.5 9B; -/// - above 64 GB: Qwen3.8 27B. +/// - 32 through 79 GB: Qwen3.5 9B; +/// - 80 GB and above: Qwen3.8 27B. /// /// The Qwen entries are canonicalized from mesh-llm's compiled /// `MODEL_CATALOG` rather than synthesized. @@ -28,13 +28,15 @@ const CURATED_MEDIUM: &str = "unsloth/Qwen3.5-9B-GGUF:Q4_K_M"; const CURATED_MEDIUM_ALIAS: &str = "Qwen3.5-9B-Vision-Q4_K_M"; const CURATED_SMALL: &str = "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M"; const CURATED_SMALL_ALIAS: &str = "Gemma-4-E4B-it-Q4_K_M"; -/// Superseded large pick, still mapped so machines that already selected it -/// keep resolving to a model id Mesh's OpenAI ingress accepts. +/// Superseded large alias retained only to preserve the historical string +/// canonicalization contract. Availability in a particular Mesh runtime is +/// determined by that runtime's compiled catalog. const LEGACY_LARGE: &str = "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M"; const LEGACY_LARGE_ALIAS: &str = "gemma-4-26B-A4B-it-UD-Q4_K_M"; /// Rated-capacity boundary for the balanced Qwen3.5 9B tier. const CURATED_MEDIUM_MIN_RATED_GB: u64 = 32; -/// Qwen3.8 27B is reserved for rated capacity classes above 64 GB. +/// Qwen3.8 27B is reserved for 80 GB-and-larger rated capacity classes, +/// leaving headroom beyond the model weights for KV cache and runtime use. const CURATED_LARGE_MIN_RATED_GB: u64 = 80; /// The Buzz-curated recommendation for a machine's rated memory capacity. @@ -266,7 +268,7 @@ mod tests { assert_eq!(CURATED_MEDIUM, "unsloth/Qwen3.5-9B-GGUF:Q4_K_M"); assert_eq!(CURATED_LARGE, "unsloth/Qwen3.8-27B-GGUF:Q4_K_M"); - // Qwen3.8 is reserved for rated capacity classes above 64 GB. + // Qwen3.8 is reserved for 80 GB-and-larger rated capacity classes. let large = build_catalog(None, 80_000_000_000, 80.0, &[]); assert_eq!(large.recommended.as_deref(), Some(CURATED_LARGE)); let big = build_catalog(None, 128_000_000_000, 128.0, &[]); diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 92693974cf6..75ecc4c10b5 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -11,6 +11,10 @@ use std::time::Duration; use nostr::Tag; use tauri::{AppHandle, Manager}; +use super::coordinator_admission::{ + clear_mode_recovery_restart, mark_mode_recovery_restart, mode_reconcile_action, + mode_recovery_restart_is_throttled, ModeEvidence, ModeReconcileAction, ModeReconcileState, +}; use crate::app_state::AppState; /// Client-owned parameterized-replaceable discovery note. We use the standard @@ -32,7 +36,6 @@ const INGRESS_WATCHDOG_MAX: Duration = Duration::from_secs(120); /// 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<()>, @@ -63,12 +66,15 @@ pub async fn start_coordinator(app: AppHandle) { }); let roster_app = app.clone(); let roster_watcher = tokio::spawn(async move { - // Carries a shrink awaiting confirmation across polls (hysteresis): - // a reduced roster must be seen twice in a row before we tear down. + // Carries pending observations across polls. Roster reductions and + // admission-mode changes must both be seen twice before a restart. let mut pending_shrink: Option> = None; + let mut mode_reconcile = ModeReconcileState::default(); loop { tokio::time::sleep(ROSTER_POLL_INTERVAL).await; - if let Err(error) = reconcile_roster(&roster_app, &mut pending_shrink).await { + if let Err(error) = + reconcile_roster(&roster_app, &mut pending_shrink, &mut mode_reconcile).await + { eprintln!("buzz-mesh: roster reconcile failed: {error}"); } } @@ -297,8 +303,8 @@ fn roster_reconcile_action( } } -fn runtime_needs_mode_probe(current_owners: Option<&[String]>) -> bool { - current_owners.is_none_or(<[String]>::is_empty) +fn runtime_needs_mode_probe(current_owners: &[String]) -> bool { + current_owners.is_empty() } #[derive(Debug, PartialEq, Eq)] @@ -309,13 +315,6 @@ enum OpenRuntimeObservation { Unknown(String), } -fn open_runtime_should_restart(observation: &OpenRuntimeObservation) -> bool { - matches!( - observation, - OpenRuntimeObservation::ClosedWithRoster | OpenRuntimeObservation::ClosedWithoutRoster(_) - ) -} - fn classify_open_runtime_observation( mode: Result, closed_roster: Option, String>>, @@ -346,6 +345,7 @@ async fn observe_open_runtime_relay(state: &AppState, relay_url: &str) -> OpenRu async fn reconcile_roster( app: &AppHandle, pending_shrink: &mut Option>, + mode_reconcile: &mut ModeReconcileState, ) -> Result<(), String> { let state = app.state::(); let current_request = { @@ -354,6 +354,7 @@ async fn reconcile_roster( Some(runtime) => runtime.start_request().clone(), None => { *pending_shrink = None; + *mode_reconcile = ModeReconcileState::default(); return Ok(()); } } @@ -368,35 +369,52 @@ async fn reconcile_roster( .map(str::to_owned) .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); if current_owners.is_none() { + clear_mode_recovery_restart(app); + } + let Some(current_owners) = current_owners else { let observation = observe_open_runtime_relay(&state, &relay_url).await; - if open_runtime_should_restart(&observation) { - match &observation { - OpenRuntimeObservation::ClosedWithRoster => eprintln!( - "buzz-mesh: relay now enforces membership; restarting Buzz to apply its allowlist" - ), - OpenRuntimeObservation::ClosedWithoutRoster(error) => eprintln!( - "buzz-mesh: relay now enforces membership but its roster is unavailable; restarting Buzz fail-closed: {error}" - ), - OpenRuntimeObservation::Open | OpenRuntimeObservation::Unknown(_) => {} + let evidence = match &observation { + OpenRuntimeObservation::Open => Ok(ModeEvidence::Open), + OpenRuntimeObservation::ClosedWithRoster + | OpenRuntimeObservation::ClosedWithoutRoster(_) => Ok(ModeEvidence::Closed), + OpenRuntimeObservation::Unknown(_) => Err(()), + }; + match mode_reconcile_action(mode_reconcile, evidence, true) { + ModeReconcileAction::Keep => { + if let OpenRuntimeObservation::Unknown(error) = observation { + eprintln!( + "buzz-mesh: open-relay mode recheck failed once; keeping the running policy pending one retry: {error}" + ); + } + } + ModeReconcileAction::AwaitConfirm => eprintln!( + "buzz-mesh: relay membership mode changed; awaiting confirmation before restarting Buzz" + ), + ModeReconcileAction::RestartProcess => { + match &observation { + OpenRuntimeObservation::ClosedWithRoster => eprintln!( + "buzz-mesh: relay repeatedly reports membership enforcement; restarting Buzz to apply its allowlist" + ), + OpenRuntimeObservation::ClosedWithoutRoster(error) => eprintln!( + "buzz-mesh: relay repeatedly reports membership enforcement but its roster is unavailable; restarting Buzz fail-closed: {error}" + ), + OpenRuntimeObservation::Unknown(error) => eprintln!( + "buzz-mesh: open-relay mode remained unknown across two polls; restarting Buzz fail-closed: {error}" + ), + OpenRuntimeObservation::Open => {} + } + app.request_restart(); } - app.request_restart(); - } else if let OpenRuntimeObservation::Unknown(error) = observation { - eprintln!( - "buzz-mesh: open-relay mode recheck failed; keeping the running policy: {error}" - ); } *pending_shrink = None; return Ok(()); - } - let Some(current_owners) = current_owners else { - return Ok(()); }; // A failed roster query must NOT be treated as "the roster became empty": // doing so would restart the node down to self-only and de-admit every // 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 = if runtime_needs_mode_probe(Some(current_owners)) { + let query = if runtime_needs_mode_probe(current_owners) { // Self-only is the fail-safe startup policy when the initial NIP-11 // probe is inconclusive. Re-probe this isolated state so a later valid // open result can recover it; errors still keep it self-only. @@ -406,6 +424,38 @@ async fn reconcile_roster( .await .map(Some) }; + if current_owners.is_empty() { + match &query { + Ok(None) => { + match mode_reconcile_action(mode_reconcile, Ok(ModeEvidence::Open), false) { + ModeReconcileAction::Keep => return Ok(()), + ModeReconcileAction::AwaitConfirm => { + eprintln!( + "buzz-mesh: isolated startup fallback observed an open relay; awaiting confirmation before restarting Buzz" + ); + return Ok(()); + } + ModeReconcileAction::RestartProcess => { + if mode_recovery_restart_is_throttled(app)? { + eprintln!( + "buzz-mesh: open-mode recovery restart is throttled after a recent attempt; keeping the self-only fallback" + ); + return Ok(()); + } + mark_mode_recovery_restart(app)?; + } + } + } + Err(_) => { + let _ = mode_reconcile_action(mode_reconcile, Err(()), false); + } + Ok(Some(_)) => { + let _ = mode_reconcile_action(mode_reconcile, Ok(ModeEvidence::Closed), false); + } + } + } else { + *mode_reconcile = ModeReconcileState::default(); + } match roster_reconcile_action(current_owners, pending_shrink.as_deref(), query) { RosterReconcileAction::Keep => { *pending_shrink = None; @@ -766,12 +816,11 @@ mod tests { } #[test] - fn only_open_or_self_only_runtimes_reprobe_mode() { + fn only_self_only_closed_runtimes_reprobe_mode() { let self_only: Vec = Vec::new(); let closed = vec!["owner-a".to_string()]; - assert!(runtime_needs_mode_probe(None)); - assert!(runtime_needs_mode_probe(Some(&self_only))); - assert!(!runtime_needs_mode_probe(Some(&closed))); + assert!(runtime_needs_mode_probe(&self_only)); + assert!(!runtime_needs_mode_probe(&closed)); } #[test] @@ -786,23 +835,6 @@ mod tests { "relay returned no membership snapshot".to_string() ) ); - assert!(open_runtime_should_restart(&observation)); - } - - #[test] - fn open_runtime_restarts_for_any_proven_closed_mode() { - assert!(!open_runtime_should_restart(&OpenRuntimeObservation::Open)); - assert!(open_runtime_should_restart( - &OpenRuntimeObservation::ClosedWithRoster - )); - assert!(open_runtime_should_restart( - &OpenRuntimeObservation::ClosedWithoutRoster( - "relay returned no membership snapshot".to_string() - ) - )); - assert!(!open_runtime_should_restart( - &OpenRuntimeObservation::Unknown("NIP-11 timed out".to_string()) - )); } #[test] diff --git a/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs b/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs new file mode 100644 index 00000000000..19806d770a5 --- /dev/null +++ b/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs @@ -0,0 +1,163 @@ +use std::time::Duration; + +use tauri::{AppHandle, Manager}; + +/// Prevent a flaky startup mode probe from creating an unbounded whole-app +/// restart loop while recovering a self-only fallback into open mode. +const MODE_RECOVERY_RESTART_COOLDOWN: Duration = Duration::from_secs(10 * 60); +const MODE_RECOVERY_RESTART_MARKER: &str = "mesh-mode-recovery-restart"; + +fn mode_recovery_restart_marker(app: &AppHandle) -> Result { + Ok(app + .path() + .app_data_dir() + .map_err(|error| format!("failed to resolve app data directory: {error}"))? + .join(MODE_RECOVERY_RESTART_MARKER)) +} + +pub(super) fn mode_recovery_restart_is_throttled(app: &AppHandle) -> Result { + let marker = mode_recovery_restart_marker(app)?; + let modified = match std::fs::metadata(marker).and_then(|metadata| metadata.modified()) { + Ok(modified) => modified, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(error) => return Err(format!("failed to read mesh restart marker: {error}")), + }; + Ok(modified + .elapsed() + .is_ok_and(|elapsed| elapsed < MODE_RECOVERY_RESTART_COOLDOWN)) +} + +pub(super) fn mark_mode_recovery_restart(app: &AppHandle) -> Result<(), String> { + let marker = mode_recovery_restart_marker(app)?; + if let Some(parent) = marker.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("failed to create app data directory: {error}"))?; + } + std::fs::write(marker, b"self-only-to-open\n") + .map_err(|error| format!("failed to write mesh restart marker: {error}")) +} + +pub(super) fn clear_mode_recovery_restart(app: &AppHandle) { + if let Ok(marker) = mode_recovery_restart_marker(app) { + match std::fs::remove_file(marker) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => eprintln!("buzz-mesh: failed to clear mesh restart marker: {error}"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ModeEvidence { + Open, + Closed, +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub(super) struct ModeReconcileState { + pending: Option, + consecutive_unknown: u8, +} + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum ModeReconcileAction { + Keep, + AwaitConfirm, + RestartProcess, +} + +/// Require two consecutive mode observations before restarting the whole app. +/// Unknown observations preserve a running policy once, but a second +/// consecutive unknown while admission is unenforced restarts fail-closed. +pub(super) fn mode_reconcile_action( + state: &mut ModeReconcileState, + evidence: Result, + running_open: bool, +) -> ModeReconcileAction { + match evidence { + Err(()) => { + state.pending = None; + state.consecutive_unknown = state.consecutive_unknown.saturating_add(1); + if running_open && state.consecutive_unknown >= 2 { + state.consecutive_unknown = 0; + ModeReconcileAction::RestartProcess + } else { + ModeReconcileAction::Keep + } + } + Ok(observed) => { + state.consecutive_unknown = 0; + let transition = (running_open && observed == ModeEvidence::Closed) + || (!running_open && observed == ModeEvidence::Open); + if !transition { + state.pending = None; + return ModeReconcileAction::Keep; + } + if state.pending == Some(observed) { + state.pending = None; + ModeReconcileAction::RestartProcess + } else { + state.pending = Some(observed); + ModeReconcileAction::AwaitConfirm + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn transition_requires_two_matching_observations() { + let mut state = ModeReconcileState::default(); + assert_eq!( + mode_reconcile_action(&mut state, Ok(ModeEvidence::Closed), true), + ModeReconcileAction::AwaitConfirm + ); + assert_eq!( + mode_reconcile_action(&mut state, Ok(ModeEvidence::Closed), true), + ModeReconcileAction::RestartProcess + ); + } + + #[test] + fn recovery_cancels_pending_restart() { + let mut state = ModeReconcileState::default(); + assert_eq!( + mode_reconcile_action(&mut state, Ok(ModeEvidence::Closed), true), + ModeReconcileAction::AwaitConfirm + ); + assert_eq!( + mode_reconcile_action(&mut state, Ok(ModeEvidence::Open), true), + ModeReconcileAction::Keep + ); + assert_eq!(state, ModeReconcileState::default()); + } + + #[test] + fn persistently_unknown_open_runtime_restarts_fail_closed() { + let mut state = ModeReconcileState::default(); + assert_eq!( + mode_reconcile_action(&mut state, Err(()), true), + ModeReconcileAction::Keep + ); + assert_eq!( + mode_reconcile_action(&mut state, Err(()), true), + ModeReconcileAction::RestartProcess + ); + } + + #[test] + fn isolated_open_recovery_requires_confirmation() { + let mut state = ModeReconcileState::default(); + assert_eq!( + mode_reconcile_action(&mut state, Ok(ModeEvidence::Open), false), + ModeReconcileAction::AwaitConfirm + ); + assert_eq!( + mode_reconcile_action(&mut state, Ok(ModeEvidence::Open), false), + ModeReconcileAction::RestartProcess + ); + } +} diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index d6a2b925dba..1aa7ead858e 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; mod coordinator; +mod coordinator_admission; pub(crate) use coordinator::{publish_current_status_once, publish_stopped_status_once_at}; pub use coordinator::{start_coordinator, MeshCoordinator, KIND_BUZZ_MESH_MEMBER_STATUS}; From 502eed9028c8905937783c7353829f1e78bb685c Mon Sep 17 00:00:00 2001 From: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Date: Sat, 29 Aug 2026 09:04:19 +1000 Subject: [PATCH 17/20] fix(mesh): close admission restart loop Apply one persisted cooldown and in-memory latch to both admission-mode restart paths, retain throttling across clock rollback, and cover the composed self-only hysteresis wiring. Make the shared Tauri clippy recipe feature-enable mesh support so local checks and CI stay aligned. Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> --- .github/workflows/ci.yml | 4 - Justfile | 4 +- desktop/src-tauri/src/mesh_llm/coordinator.rs | 149 +++++++++++------- .../src/mesh_llm/coordinator_admission.rs | 97 ++++++++++-- 4 files changed, 179 insertions(+), 75 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87c45a8b4ab..00474ce0ff3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,10 +214,6 @@ jobs: run: just desktop-tauri-test env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Desktop Mesh clippy - run: cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --features mesh-llm -- -D warnings - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - name: Desktop Mesh admission tests run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm --lib env: diff --git a/Justfile b/Justfile index c2eaf2ed5e0..038f575bad2 100644 --- a/Justfile +++ b/Justfile @@ -210,8 +210,10 @@ _ensure-migrations: _ensure-services ./scripts/seed-local-community.sh # Run clippy on the desktop Tauri Rust crate +# Mesh support is part of the shipped desktop contract, so local `just check` +# and CI both lint the same feature-enabled graph. desktop-tauri-clippy: _ensure-sidecar-stubs - cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets -- -D warnings + cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets --features mesh-llm -- -D warnings # Check the desktop Tauri Rust crate compiles desktop-tauri-check: _ensure-sidecar-stubs diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 75ecc4c10b5..5de0cd2c867 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -12,8 +12,8 @@ use nostr::Tag; use tauri::{AppHandle, Manager}; use super::coordinator_admission::{ - clear_mode_recovery_restart, mark_mode_recovery_restart, mode_reconcile_action, - mode_recovery_restart_is_throttled, ModeEvidence, ModeReconcileAction, ModeReconcileState, + clear_mode_restart, mode_reconcile_action, request_mode_restart, ModeEvidence, + ModeReconcileAction, ModeReconcileState, }; use crate::app_state::AppState; @@ -342,11 +342,42 @@ async fn observe_open_runtime_relay(state: &AppState, relay_url: &str) -> OpenRu classify_open_runtime_observation(mode, closed_roster) } +#[derive(Debug, PartialEq, Eq)] +enum SelfOnlyModeAction { + Keep, + AwaitConfirm, + RestartProcess, +} + +fn reconcile_self_only_mode( + state: &mut ModeReconcileState, + query: &Result>, String>, +) -> SelfOnlyModeAction { + match query { + Ok(None) => match mode_reconcile_action(state, Ok(ModeEvidence::Open), false) { + ModeReconcileAction::Keep => SelfOnlyModeAction::Keep, + ModeReconcileAction::AwaitConfirm => SelfOnlyModeAction::AwaitConfirm, + ModeReconcileAction::RestartProcess => SelfOnlyModeAction::RestartProcess, + }, + Err(_) => { + let _ = mode_reconcile_action(state, Err(()), false); + SelfOnlyModeAction::Keep + } + Ok(Some(_)) => { + let _ = mode_reconcile_action(state, Ok(ModeEvidence::Closed), false); + SelfOnlyModeAction::Keep + } + } +} + async fn reconcile_roster( app: &AppHandle, pending_shrink: &mut Option>, mode_reconcile: &mut ModeReconcileState, ) -> Result<(), String> { + if mode_reconcile.restart_requested() { + return Ok(()); + } let state = app.state::(); let current_request = { let runtime = state.mesh_llm_runtime.lock().await; @@ -368,9 +399,6 @@ async fn reconcile_roster( .as_deref() .map(str::to_owned) .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); - if current_owners.is_none() { - clear_mode_recovery_restart(app); - } let Some(current_owners) = current_owners else { let observation = observe_open_runtime_relay(&state, &relay_url).await; let evidence = match &observation { @@ -391,19 +419,32 @@ async fn reconcile_roster( "buzz-mesh: relay membership mode changed; awaiting confirmation before restarting Buzz" ), ModeReconcileAction::RestartProcess => { - match &observation { - OpenRuntimeObservation::ClosedWithRoster => eprintln!( - "buzz-mesh: relay repeatedly reports membership enforcement; restarting Buzz to apply its allowlist" - ), - OpenRuntimeObservation::ClosedWithoutRoster(error) => eprintln!( - "buzz-mesh: relay repeatedly reports membership enforcement but its roster is unavailable; restarting Buzz fail-closed: {error}" - ), - OpenRuntimeObservation::Unknown(error) => eprintln!( - "buzz-mesh: open-relay mode remained unknown across two polls; restarting Buzz fail-closed: {error}" - ), - OpenRuntimeObservation::Open => {} + let reason = match &observation { + OpenRuntimeObservation::ClosedWithRoster => "open-to-closed", + OpenRuntimeObservation::ClosedWithoutRoster(_) => { + "open-to-closed-without-roster" + } + OpenRuntimeObservation::Unknown(_) => "open-mode-unknown", + OpenRuntimeObservation::Open => return Ok(()), + }; + if !request_mode_restart(app, mode_reconcile, reason)? { + eprintln!( + "buzz-mesh: mode-policy restart is already pending or throttled after a recent attempt; keeping the running policy" + ); + } else { + match &observation { + OpenRuntimeObservation::ClosedWithRoster => eprintln!( + "buzz-mesh: relay repeatedly reports membership enforcement; restarting Buzz to apply its allowlist" + ), + OpenRuntimeObservation::ClosedWithoutRoster(error) => eprintln!( + "buzz-mesh: relay repeatedly reports membership enforcement but its roster is unavailable; restarting Buzz fail-closed: {error}" + ), + OpenRuntimeObservation::Unknown(error) => eprintln!( + "buzz-mesh: open-relay mode remained unknown across two polls; restarting Buzz fail-closed: {error}" + ), + OpenRuntimeObservation::Open => {} + } } - app.request_restart(); } } *pending_shrink = None; @@ -424,33 +465,27 @@ async fn reconcile_roster( .await .map(Some) }; + if matches!(query, Ok(Some(_))) { + // A successful closed-roster read is decisive non-restarting mode + // evidence, so a prior cross-process mode-restart throttle may clear. + clear_mode_restart(app); + } if current_owners.is_empty() { - match &query { - Ok(None) => { - match mode_reconcile_action(mode_reconcile, Ok(ModeEvidence::Open), false) { - ModeReconcileAction::Keep => return Ok(()), - ModeReconcileAction::AwaitConfirm => { - eprintln!( - "buzz-mesh: isolated startup fallback observed an open relay; awaiting confirmation before restarting Buzz" + match reconcile_self_only_mode(mode_reconcile, &query) { + SelfOnlyModeAction::Keep => {} + SelfOnlyModeAction::AwaitConfirm => { + eprintln!( + "buzz-mesh: isolated startup fallback observed an open relay; awaiting confirmation before restarting Buzz" + ); + return Ok(()); + } + SelfOnlyModeAction::RestartProcess => { + if !request_mode_restart(app, mode_reconcile, "self-only-to-open")? { + eprintln!( + "buzz-mesh: open-mode recovery restart is already pending or throttled after a recent attempt; keeping the self-only fallback" ); - return Ok(()); - } - ModeReconcileAction::RestartProcess => { - if mode_recovery_restart_is_throttled(app)? { - eprintln!( - "buzz-mesh: open-mode recovery restart is throttled after a recent attempt; keeping the self-only fallback" - ); - return Ok(()); - } - mark_mode_recovery_restart(app)?; - } } - } - Err(_) => { - let _ = mode_reconcile_action(mode_reconcile, Err(()), false); - } - Ok(Some(_)) => { - let _ = mode_reconcile_action(mode_reconcile, Ok(ModeEvidence::Closed), false); + return Ok(()); } } } else { @@ -714,21 +749,27 @@ mod tests { ); } - // Regression: a transient NIP-11 probe failure at startup on an OPEN relay - // falls back to the enforcing path and starts the node with a self-only - // allowlist. If reconcile then treated `Ok(None)` as an unconditional - // `Keep`, that node would sit at "1 NODE" until an app restart — the - // original open-relay bug, reintroduced by a single HTTP blip. A self-only - // roster admits nobody else, so restarting to run unenforced relaxes - // nothing that was protecting anyone. #[test] - fn open_relay_recovers_a_fallback_isolated_node() { - let self_only: Vec = Vec::new(); - let action = roster_reconcile_action(&self_only, None, Ok(None)); + fn self_only_wiring_never_restarts_after_one_open_observation() { + let mut state = ModeReconcileState::default(); + let query = Ok(None); assert_eq!( - action, - RosterReconcileAction::RestartProcess, - "a fallback-isolated node on an open relay must restart into open mesh" + reconcile_self_only_mode(&mut state, &query), + SelfOnlyModeAction::AwaitConfirm + ); + } + + #[test] + fn self_only_wiring_restarts_after_two_open_observations() { + let mut state = ModeReconcileState::default(); + let query = Ok(None); + assert_eq!( + reconcile_self_only_mode(&mut state, &query), + SelfOnlyModeAction::AwaitConfirm + ); + assert_eq!( + reconcile_self_only_mode(&mut state, &query), + SelfOnlyModeAction::RestartProcess ); } diff --git a/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs b/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs index 19806d770a5..54d8ccc77be 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs @@ -2,43 +2,54 @@ use std::time::Duration; use tauri::{AppHandle, Manager}; -/// Prevent a flaky startup mode probe from creating an unbounded whole-app -/// restart loop while recovering a self-only fallback into open mode. -const MODE_RECOVERY_RESTART_COOLDOWN: Duration = Duration::from_secs(10 * 60); -const MODE_RECOVERY_RESTART_MARKER: &str = "mesh-mode-recovery-restart"; +/// Prevent a flaky mode probe from creating an unbounded whole-app restart +/// loop while changing admission policy. +const MODE_RESTART_COOLDOWN: Duration = Duration::from_secs(10 * 60); +const MODE_RESTART_MARKER: &str = "mesh-mode-recovery-restart"; -fn mode_recovery_restart_marker(app: &AppHandle) -> Result { +fn mode_restart_marker(app: &AppHandle) -> Result { Ok(app .path() .app_data_dir() .map_err(|error| format!("failed to resolve app data directory: {error}"))? - .join(MODE_RECOVERY_RESTART_MARKER)) + .join(MODE_RESTART_MARKER)) } -pub(super) fn mode_recovery_restart_is_throttled(app: &AppHandle) -> Result { - let marker = mode_recovery_restart_marker(app)?; +fn restart_marker_is_throttled( + modified: std::time::SystemTime, + now: std::time::SystemTime, +) -> bool { + // A clock rollback makes the marker appear to be in the future. Fail safe: + // retain the throttle rather than disabling the only cross-process bound. + now.duration_since(modified) + .map_or(true, |elapsed| elapsed < MODE_RESTART_COOLDOWN) +} + +fn mode_restart_is_throttled(app: &AppHandle) -> Result { + let marker = mode_restart_marker(app)?; let modified = match std::fs::metadata(marker).and_then(|metadata| metadata.modified()) { Ok(modified) => modified, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), Err(error) => return Err(format!("failed to read mesh restart marker: {error}")), }; - Ok(modified - .elapsed() - .is_ok_and(|elapsed| elapsed < MODE_RECOVERY_RESTART_COOLDOWN)) + Ok(restart_marker_is_throttled( + modified, + std::time::SystemTime::now(), + )) } -pub(super) fn mark_mode_recovery_restart(app: &AppHandle) -> Result<(), String> { - let marker = mode_recovery_restart_marker(app)?; +fn mark_mode_restart(app: &AppHandle, reason: &str) -> Result<(), String> { + let marker = mode_restart_marker(app)?; if let Some(parent) = marker.parent() { std::fs::create_dir_all(parent) .map_err(|error| format!("failed to create app data directory: {error}"))?; } - std::fs::write(marker, b"self-only-to-open\n") + std::fs::write(marker, format!("{reason}\n")) .map_err(|error| format!("failed to write mesh restart marker: {error}")) } -pub(super) fn clear_mode_recovery_restart(app: &AppHandle) { - if let Ok(marker) = mode_recovery_restart_marker(app) { +pub(super) fn clear_mode_restart(app: &AppHandle) { + if let Ok(marker) = mode_restart_marker(app) { match std::fs::remove_file(marker) { Ok(()) => {} Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} @@ -57,6 +68,22 @@ pub(super) enum ModeEvidence { pub(super) struct ModeReconcileState { pending: Option, consecutive_unknown: u8, + restart_requested: bool, +} + +impl ModeReconcileState { + pub(super) fn restart_requested(&self) -> bool { + self.restart_requested + } + + pub(super) fn latch_restart(&mut self) -> bool { + if self.restart_requested { + false + } else { + self.restart_requested = true; + true + } + } } #[derive(Debug, PartialEq, Eq)] @@ -66,6 +93,22 @@ pub(super) enum ModeReconcileAction { RestartProcess, } +pub(super) fn request_mode_restart( + app: &AppHandle, + state: &mut ModeReconcileState, + reason: &str, +) -> Result { + if state.restart_requested || mode_restart_is_throttled(app)? { + return Ok(false); + } + mark_mode_restart(app, reason)?; + if !state.latch_restart() { + return Ok(false); + } + app.request_restart(); + Ok(true) +} + /// Require two consecutive mode observations before restarting the whole app. /// Unknown observations preserve a running policy once, but a second /// consecutive unknown while admission is unenforced restarts fail-closed. @@ -108,6 +151,28 @@ pub(super) fn mode_reconcile_action( mod tests { use super::*; + #[test] + fn restart_latch_only_arms_once() { + let mut state = ModeReconcileState::default(); + assert!(state.latch_restart()); + assert!(state.restart_requested()); + assert!(!state.latch_restart()); + } + + #[test] + fn clock_rollback_keeps_restart_throttled() { + let now = std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(100); + let future_marker = now + Duration::from_secs(1); + assert!(restart_marker_is_throttled(future_marker, now)); + } + + #[test] + fn old_restart_marker_is_not_throttled() { + let marker = std::time::SystemTime::UNIX_EPOCH; + let now = marker + MODE_RESTART_COOLDOWN + Duration::from_secs(1); + assert!(!restart_marker_is_throttled(marker, now)); + } + #[test] fn transition_requires_two_matching_observations() { let mut state = ModeReconcileState::default(); From d0ebf66117c20830bb97f3e00b0a6c07fef195ba Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Sat, 29 Aug 2026 10:59:31 +1000 Subject: [PATCH 18/20] revert(mesh): drop open-relay unenforced mode; mesh stays allowlist-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the open-relay/open-mesh admission mode from PR #6189 at Mic's direction: running the mesh unenforced when the Buzz relay does not enforce NIP-43 membership is not a product mode we want, and it was the source of all the fail-open complexity (NIP-11 mode probing, the two-observation mode state machine, restart markers/throttles/latches). Reverted to main's behavior: admission is always roster/allowlist driven. On a relay with no membership snapshot the mesh simply runs self-only — safe and boring, no mode probes, no whole-app restarts. Removed: MeshRelayMode, relay_mesh_mode_from_nip11, the NIP-11 mode probe, coordinator_admission.rs, mode-scoped discovery/availability, and all their tests. Kept from this branch (independent of the mode work): - mesh-llm 0.76.0-rc7 upgrade and the restored hardware model ladder - BUZZ_AGENT_LLM_TIMEOUT_SECS=660 mesh client timeout default - desktop-standalone mesh=1 feature flag wiring - cfg-only warning fixes in workspace.rs and restore.rs desktop-tauri-clippy now lints BOTH cfg graphs (default and --features mesh-llm): features are additive, so a single invocation covers only one graph, and both ship (release-windows builds without mesh-llm). CI keeps calling the recipe, and keeps the feature-enabled --lib test step. Verified on this commit: clippy -D warnings clean on both graphs; feature-enabled lib suite 3110 passed / 0 failed / 19 ignored; default-feature lib suite 3005 passed / 0 failed; fmt and diff checks clean. Signed-off-by: Michael Neale --- .github/workflows/ci.yml | 2 +- Justfile | 7 +- .../src-tauri/src/commands/agent_models.rs | 13 +- desktop/src-tauri/src/commands/mesh_llm.rs | 173 ++-------- .../src-tauri/src/commands/mesh_llm_tests.rs | 78 ----- desktop/src-tauri/src/mesh_llm/coordinator.rs | 297 ++---------------- .../src/mesh_llm/coordinator_admission.rs | 228 -------------- desktop/src-tauri/src/mesh_llm/discovery.rs | 111 +------ desktop/src-tauri/src/mesh_llm/mod.rs | 6 +- desktop/src-tauri/src/mesh_llm/mod_tests.rs | 170 ---------- 10 files changed, 74 insertions(+), 1011 deletions(-) delete mode 100644 desktop/src-tauri/src/mesh_llm/coordinator_admission.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00474ce0ff3..3eec4dc2e84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: run: just desktop-tauri-test env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Desktop Mesh admission tests + - name: Desktop Mesh feature tests run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm --lib env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" diff --git a/Justfile b/Justfile index 038f575bad2..4df797071ab 100644 --- a/Justfile +++ b/Justfile @@ -210,9 +210,12 @@ _ensure-migrations: _ensure-services ./scripts/seed-local-community.sh # Run clippy on the desktop Tauri Rust crate -# Mesh support is part of the shipped desktop contract, so local `just check` -# and CI both lint the same feature-enabled graph. +# Features are additive, so a single invocation lints only one cfg graph. +# Both graphs ship (release-windows builds without mesh-llm), so lint both: +# the default graph covers the `#[cfg(not(feature = "mesh-llm"))]` arms and +# the feature-enabled graph covers the mesh code. desktop-tauri-clippy: _ensure-sidecar-stubs + cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets -- -D warnings cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets --features mesh-llm -- -D warnings # Check the desktop Tauri Rust crate compiles diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 7f5139a08b2..05b1abad90d 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -245,9 +245,16 @@ pub async fn discover_agent_models( if input.provider.as_deref().map(str::trim) == Some(crate::managed_agents::RELAY_MESH_PROVIDER_ID) { - let availability = crate::commands::mesh_llm::resolve_mesh_availability(&state) - .await - .map_err(|error| format!("Buzz shared compute model discovery failed: {error}"))?; + let events = crate::relay::query_relay( + &state, + &[ + crate::mesh_llm::mesh_status_filter(), + crate::mesh_llm::relay_membership_filter(), + ], + ) + .await + .map_err(|error| format!("Buzz shared compute model discovery failed: {error}"))?; + let availability = crate::mesh_llm::availability_from_events(events); if availability.models.is_empty() { return Err(availability.reason.unwrap_or_else(|| { "No live Buzz shared compute models are available".to_string() diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index e5ff5e39d56..7356cd7fc0c 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -176,55 +176,9 @@ fn advance_mesh_status_cursor( Ok(cursor) } -/// Resolve whether a relay enforces NIP-43 relay membership, from its NIP-11 -/// document. -/// -/// The relay's advertisement is authoritative only to the extent that the -/// HTTP transport is authenticated: `wss://` relay URLs produce HTTPS NIP-11 -/// reads, while a `ws://` override is vulnerable to on-path modification. -/// -/// The relay advertises NIP-43 only when membership is actually enforced -/// (`nip11_facts` in `buzz-relay`), so this is the one signal that separates -/// "no roster because nobody is a direct member here" from "no roster because -/// this read was incomplete". A probe failure is reported as `Err` and treated -/// as enforcing by callers: an unreachable relay must never be able to relax -/// admission. -async fn probe_relay_mesh_mode(relay_url: &str) -> Result { - let http_url = relay::relay_http_base_url(relay_url); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(5)) - .build() - .map_err(|error| format!("failed to build NIP-11 client: {error}"))?; - let document: serde_json::Value = client - .get(&http_url) - .header("Accept", "application/nostr+json") - .send() - .await - .map_err(|error| format!("NIP-11 probe failed: {error}"))? - .error_for_status() - .map_err(|error| format!("NIP-11 probe returned an unsuccessful HTTP status: {error}"))? - .json() - .await - .map_err(|error| format!("NIP-11 document was not JSON: {error}"))?; - mesh_llm::relay_mesh_mode_from_nip11(&document) -} - -/// Probe the relay's current membership mode. -/// -/// Only a successful, strictly valid NIP-11 document proves a mode. Transport, -/// HTTP, and document errors remain errors so callers can preserve a running -/// admission policy instead of turning an observation failure into a mode -/// transition. -pub(crate) async fn resolved_relay_mesh_mode( - relay_url: &str, -) -> Result { - probe_relay_mesh_mode(relay_url).await -} - -async fn query_mesh_discovery_events_for_mode( +async fn query_mesh_discovery_events_at( state: &AppState, relay_url: &str, - mode: mesh_llm::MeshRelayMode, ) -> Result, String> { let api_base_url = relay::relay_http_base_url(relay_url); let mut events = @@ -239,27 +193,13 @@ async fn query_mesh_discovery_events_for_mode( // means the query is incomplete: surface it as an error so the // reconcile loop keeps the current allowlist instead of flapping the // node down to self-only on a successful-but-empty response. - // - // A relay that does not enforce membership never populates - // `relay_members` from sign-in, so there is no roster to wait for and - // no incomplete read to guard against. Fall through and read status - // notes unfiltered so the caller can run the mesh unenforced. - if !mode.is_open() { - if !mesh_llm::has_membership_snapshot(&events) { - return Err("relay returned no membership snapshot".to_string()); - } - return Ok(events); + if !mesh_llm::has_membership_snapshot(&events) { + return Err("relay returned no membership snapshot".to_string()); } + return Ok(events); } let mut status_filter = mesh_llm::mesh_status_filter(); - // On an enforcing relay, scope status reads to current members. On an open - // relay the snapshot is not a trust boundary even when one exists (added - // out of band) — admission runs unenforced there, so scoping reads to a - // stray roster would hide peers the mesh admits. Read every reporter's - // note and let freshness + endpoint-binding checks gate routing. - if !mode.is_open() && !member_pubkeys.is_empty() { - status_filter["authors"] = serde_json::json!(member_pubkeys); - } + status_filter["authors"] = serde_json::json!(member_pubkeys); let mut previous_cursor: Option<(u64, String)> = None; loop { @@ -279,17 +219,8 @@ async fn query_mesh_discovery_events_for_mode( } } -/// Resolve model availability using the same relay-mode probe and discovery -/// query as mesh startup and routing. Agent model pickers must go through this -/// path too: the legacy closed-mode helper rejects every open relay because an -/// open relay correctly publishes no membership snapshot. -pub(crate) async fn resolve_mesh_availability( - state: &AppState, -) -> Result { - let relay_url = relay::relay_ws_url_with_override(state); - let mode = resolved_relay_mesh_mode(&relay_url).await?; - let events = query_mesh_discovery_events_for_mode(state, &relay_url, mode).await?; - Ok(mesh_llm::availability_from_events_for_mode(events, mode)) +async fn query_mesh_discovery_events(state: &AppState) -> Result, String> { + query_mesh_discovery_events_at(state, &relay::relay_ws_url_with_override(state)).await } /// Resolve the admission roster by intersecting member-signed mesh status @@ -300,42 +231,16 @@ pub(crate) async fn resolve_mesh_availability( /// never be collapsed into "self-only", or a transient relay blip de-admits /// every other member. `reconcile_roster` relies on this to keep the current /// allowlist on error instead of restarting the node down to self-only. -/// `None` means the relay does not enforce membership: there is no roster to -/// intersect and the node must run unenforced (`TrustPolicy::Off`), exactly as -/// stock MeshLLM does. `Some` carries an enforcing relay's roster. -pub(crate) async fn resolve_trusted_owner_ids( - state: &AppState, -) -> Result>, String> { - resolve_trusted_owner_ids_at(state, &relay::relay_ws_url_with_override(state)).await +pub(crate) async fn resolve_trusted_owner_ids(state: &AppState) -> Result, String> { + let events = query_mesh_discovery_events(state).await?; + Ok(mesh_llm::owner_ids_from_events(&events)) } pub(crate) async fn resolve_trusted_owner_ids_at( state: &AppState, relay_url: &str, -) -> Result>, String> { - let mode = resolved_relay_mesh_mode(relay_url).await?; - let events = query_mesh_discovery_events_for_mode(state, relay_url, mode).await?; - if mode.is_open() { - return Ok(None); - } - Ok(Some(mesh_llm::owner_ids_from_events(&events))) -} - -/// Refresh the roster of a runtime that is already membership-enforcing. -/// -/// Its admission mode is established, so reconciliation reads only the -/// authoritative membership/status snapshot. NIP-11 timeouts or changes cannot -/// alter this closed-runtime path. -pub(crate) async fn resolve_closed_trusted_owner_ids_at( - state: &AppState, - relay_url: &str, ) -> Result, String> { - let events = query_mesh_discovery_events_for_mode( - state, - relay_url, - mesh_llm::MeshRelayMode::ClosedMembershipEnforced, - ) - .await?; + let events = query_mesh_discovery_events_at(state, relay_url).await?; Ok(mesh_llm::owner_ids_from_events(&events)) } @@ -343,14 +248,12 @@ pub(crate) async fn resolve_closed_trusted_owner_ids_at( /// (an empty roster) when the relay query fails. This is safe only at start: /// there is no established allowlist to preserve yet. The periodic /// `reconcile_roster` path must NOT use this — it has a live roster to keep. -pub(crate) async fn resolve_trusted_owner_ids_or_self_only( - state: &AppState, -) -> Option> { +pub(crate) async fn resolve_trusted_owner_ids_or_self_only(state: &AppState) -> Vec { match resolve_trusted_owner_ids(state).await { Ok(owners) => owners, Err(error) => { eprintln!("buzz-mesh: roster query failed; allowing only this node: {error}"); - Some(Vec::new()) + Vec::new() } } } @@ -388,13 +291,12 @@ pub(crate) async fn resolve_buzz_mesh_join_targets_at( state: &AppState, relay_url: &str, ) -> Result, String> { - let mode = resolved_relay_mesh_mode(relay_url).await?; - let events = query_mesh_discovery_events_for_mode(state, relay_url, mode).await?; + let events = query_mesh_discovery_events_at(state, relay_url).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_for_mode(events, mode).serve_targets, + mesh_llm::availability_from_events(events).serve_targets, &self_owner_id, )) } @@ -406,29 +308,15 @@ pub(crate) async fn resolve_buzz_mesh_join_targets_at( async fn resolve_buzz_mesh_startup_at( state: &AppState, relay_url: &str, -) -> (Option>, Option) { - let mode = match resolved_relay_mesh_mode(relay_url).await { - Ok(mode) => mode, - Err(error) => { - // Initial startup has no established policy to preserve, so - // ambiguous mode evidence starts isolated and retries later. - eprintln!( - "buzz-mesh: startup NIP-11 probe failed; allowing only this node and starting isolated for now: {error}" - ); - return (Some(Vec::new()), None); - } - }; - match query_mesh_discovery_events_for_mode(state, relay_url, mode).await { +) -> (Vec, Option) { + match query_mesh_discovery_events_at(state, relay_url).await { Ok(events) => { - // An open relay has no roster to enforce: `None` runs the node - // unenforced instead of collapsing it to a self-only allowlist. - let trusted_owner_ids = - (!mode.is_open()).then(|| mesh_llm::owner_ids_from_events(&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_for_mode(events, mode).serve_targets, + mesh_llm::availability_from_events(events).serve_targets, &identity.owner_id, ) .into_iter() @@ -444,7 +332,7 @@ async fn resolve_buzz_mesh_startup_at( eprintln!( "buzz-mesh: startup discovery failed; allowing only this node and starting isolated for now: {error}" ); - (Some(Vec::new()), None) + (Vec::new(), None) } } } @@ -485,7 +373,7 @@ pub(crate) async fn restore_mesh_sharing(app: &AppHandle, state: &AppState) -> C join_token, mesh_name: Some(buzz_mesh_name_for_relay(&relay_url)), relay_url: Some(relay_url), - trusted_owner_ids, + trusted_owner_ids: Some(trusted_owner_ids), }; let started = mesh_llm::DesktopMeshRuntime::start(request) .await @@ -558,17 +446,10 @@ pub async fn mesh_start_node( // Frontend requests never carry a roster. Resolve it and the bootstrap // endpoint from one snapshot so UI startup does not repeat relay probes. - let roster_unresolved = request.trusted_owner_ids.is_none(); - if roster_unresolved || request.join_token.is_none() { + if request.trusted_owner_ids.is_none() || request.join_token.is_none() { let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup_at(&state, &relay_url).await; - // Adopt the resolver's decision only when the caller supplied no - // roster. `None` from the resolver is meaningful (open relay: run - // unenforced), so this cannot use `get_or_insert`, which would treat - // it as "nothing resolved" and leave the field untouched. - if roster_unresolved { - request.trusted_owner_ids = trusted_owner_ids; - } + request.trusted_owner_ids.get_or_insert(trusted_owner_ids); if request.join_token.is_none() { request.join_token = join_token; } @@ -709,7 +590,7 @@ pub(crate) async fn ensure_client_node_for_model( join_token: Some(join_token.clone()), mesh_name: Some(buzz_mesh_name(state)), relay_url: Some(relay::relay_ws_url_with_override(state)), - trusted_owner_ids: resolve_trusted_owner_ids_or_self_only(state).await, + trusted_owner_ids: Some(resolve_trusted_owner_ids_or_self_only(state).await), }; let mut runtime = state.mesh_llm_runtime.lock().await; if let Some(existing) = runtime.as_ref() { @@ -757,11 +638,9 @@ pub(crate) async fn resolve_mesh_bootstrap_target( if model_id.is_empty() { return Ok(None); } - let relay_url = relay::relay_ws_url_with_override(state); - let mode = resolved_relay_mesh_mode(&relay_url).await?; - let events = query_mesh_discovery_events_for_mode(state, &relay_url, mode).await?; + let events = query_mesh_discovery_events(state).await?; Ok(pick_serve_target_for_model( - mesh_llm::availability_from_events_for_mode(events, mode).serve_targets, + mesh_llm::availability_from_events(events).serve_targets, model_id, )) } diff --git a/desktop/src-tauri/src/commands/mesh_llm_tests.rs b/desktop/src-tauri/src/commands/mesh_llm_tests.rs index d89c1b0b736..c4e1ae2e425 100644 --- a/desktop/src-tauri/src/commands/mesh_llm_tests.rs +++ b/desktop/src-tauri/src/commands/mesh_llm_tests.rs @@ -1,6 +1,5 @@ use super::*; use crate::app_state::build_app_state; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; fn target(model_id: &str, endpoint_addr: &str) -> mesh_llm::MeshServeTarget { mesh_llm::MeshServeTarget { @@ -28,83 +27,6 @@ fn reported_target( target } -#[tokio::test] -async fn relay_mode_probe_rechecks_the_same_url_without_caching() { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind test NIP-11 server"); - let addr = listener.local_addr().expect("test server address"); - let server = tokio::spawn(async move { - for body in [ - r#"{"supported_nips":[1,11,42]}"#, - r#"{"supported_nips":[1,11,42,43]}"#, - r#"{"supported_nips":[1,"43"]}"#, - ] { - let (mut stream, _) = listener.accept().await.expect("accept NIP-11 request"); - let mut request = vec![0; 2048]; - let bytes_read = stream.read(&mut request).await.expect("read request"); - let request = String::from_utf8_lossy(&request[..bytes_read]); - assert!(request.starts_with("GET / HTTP/1.1")); - assert!(request - .to_ascii_lowercase() - .contains("accept: application/nostr+json")); - - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/nostr+json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - stream - .write_all(response.as_bytes()) - .await - .expect("write response"); - } - }); - let relay_url = format!("ws://{addr}"); - - assert_eq!( - probe_relay_mesh_mode(&relay_url).await, - Ok(mesh_llm::MeshRelayMode::OpenNoMembership) - ); - assert_eq!( - probe_relay_mesh_mode(&relay_url).await, - Ok(mesh_llm::MeshRelayMode::ClosedMembershipEnforced) - ); - assert!(probe_relay_mesh_mode(&relay_url).await.is_err()); - server.await.expect("NIP-11 server task"); -} - -#[tokio::test] -async fn relay_mode_probe_rejects_non_success_with_valid_open_body() { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("bind test NIP-11 server"); - let addr = listener.local_addr().expect("test server address"); - let server = tokio::spawn(async move { - let (mut stream, _) = listener.accept().await.expect("accept NIP-11 request"); - let mut request = vec![0; 2048]; - let bytes_read = stream.read(&mut request).await.expect("read request"); - let request = String::from_utf8_lossy(&request[..bytes_read]); - assert!(request.starts_with("GET / HTTP/1.1")); - - let body = r#"{"supported_nips":[1,11,42]}"#; - let response = format!( - "HTTP/1.1 503 Service Unavailable\r\nContent-Type: application/nostr+json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - stream - .write_all(response.as_bytes()) - .await - .expect("write response"); - }); - let relay_url = format!("ws://{addr}"); - - let error = probe_relay_mesh_mode(&relay_url) - .await - .expect_err("non-success HTTP status must not prove an open relay"); - assert!(error.contains("503 Service Unavailable"), "{error}"); - server.await.expect("NIP-11 server task"); -} - #[test] fn buzz_mesh_join_uses_the_same_live_member_from_every_other_node() { let targets = vec![ diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 5de0cd2c867..066fa463730 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -11,10 +11,6 @@ use std::time::Duration; use nostr::Tag; use tauri::{AppHandle, Manager}; -use super::coordinator_admission::{ - clear_mode_restart, mode_reconcile_action, request_mode_restart, ModeEvidence, - ModeReconcileAction, ModeReconcileState, -}; use crate::app_state::AppState; /// Client-owned parameterized-replaceable discovery note. We use the standard @@ -36,6 +32,7 @@ const INGRESS_WATCHDOG_MAX: Duration = Duration::from_secs(120); /// 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<()>, @@ -66,15 +63,12 @@ pub async fn start_coordinator(app: AppHandle) { }); let roster_app = app.clone(); let roster_watcher = tokio::spawn(async move { - // Carries pending observations across polls. Roster reductions and - // admission-mode changes must both be seen twice before a restart. + // Carries a shrink awaiting confirmation across polls (hysteresis): + // a reduced roster must be seen twice in a row before we tear down. let mut pending_shrink: Option> = None; - let mut mode_reconcile = ModeReconcileState::default(); loop { tokio::time::sleep(ROSTER_POLL_INTERVAL).await; - if let Err(error) = - reconcile_roster(&roster_app, &mut pending_shrink, &mut mode_reconcile).await - { + if let Err(error) = reconcile_roster(&roster_app, &mut pending_shrink).await { eprintln!("buzz-mesh: roster reconcile failed: {error}"); } } @@ -240,28 +234,14 @@ fn roster_shrinks(current: &[String], fresh: &[String]) -> bool { /// /// Rules: /// - query failed (`Err`) → `Keep` (never de-admit on a relay blip) -/// - relay enforces no membership (`Ok(None)`), roster beyond self → `Keep` (see below) -/// - relay enforces no membership (`Ok(None)`), self-only roster → `RestartProcess` (see below) /// - resolved roster == current → `Keep` (no-op) /// - grows (only additions) → `RestartProcess` immediately (fast admission) /// - shrinks/empties, first observation → `AwaitConfirm` (hold, re-check next poll) /// - shrinks/empties, confirmed → `RestartProcess` (same reduced roster twice) -/// -/// `Ok(None)` means the relay does not enforce NIP-43 membership, so there is -/// no roster to reconcile against. A node enforcing an allowlist that admits -/// *other* owners keeps it: relaxing a live multi-party allowlist on the -/// strength of a NIP-11 read would let a relay downgrade (or a spoofed -/// document) de-restrict a running mesh. A **self-only** allowlist is -/// different: it admits nobody else, so it protects nothing — and it is -/// exactly the fallback a transient NIP-11 probe failure at startup produces -/// on an open relay. Without recovery here that node stays at "1 NODE" until -/// an app restart (the original bug, reintroduced by a single HTTP blip). -/// Restarting resolves the roster afresh, which on an open relay yields -/// `None` → `TrustPolicy::Off`. fn roster_reconcile_action( current_owners: &[String], pending_shrink: Option<&[String]>, - query: Result>, String>, + query: Result, String>, ) -> RosterReconcileAction { let fresh = match query { Err(error) => { @@ -270,20 +250,7 @@ fn roster_reconcile_action( ); return RosterReconcileAction::Keep; } - Ok(None) if current_owners.is_empty() => { - eprintln!( - "buzz-mesh: relay does not enforce membership and this node is isolated by the \ - startup fallback; restarting to run the mesh unenforced" - ); - return RosterReconcileAction::RestartProcess; - } - Ok(None) => { - eprintln!( - "buzz-mesh: relay does not enforce membership; keeping this node's current allowlist" - ); - return RosterReconcileAction::Keep; - } - Ok(Some(fresh)) => fresh, + Ok(fresh) => fresh, }; if fresh == current_owners { @@ -303,81 +270,10 @@ fn roster_reconcile_action( } } -fn runtime_needs_mode_probe(current_owners: &[String]) -> bool { - current_owners.is_empty() -} - -#[derive(Debug, PartialEq, Eq)] -enum OpenRuntimeObservation { - Open, - ClosedWithRoster, - ClosedWithoutRoster(String), - Unknown(String), -} - -fn classify_open_runtime_observation( - mode: Result, - closed_roster: Option, String>>, -) -> OpenRuntimeObservation { - match mode { - Ok(mode) if mode.is_open() => OpenRuntimeObservation::Open, - Ok(_) => match closed_roster { - Some(Ok(_)) => OpenRuntimeObservation::ClosedWithRoster, - Some(Err(error)) => OpenRuntimeObservation::ClosedWithoutRoster(error), - None => OpenRuntimeObservation::ClosedWithoutRoster( - "closed relay roster was not queried".to_string(), - ), - }, - Err(error) => OpenRuntimeObservation::Unknown(error), - } -} - -async fn observe_open_runtime_relay(state: &AppState, relay_url: &str) -> OpenRuntimeObservation { - let mode = crate::commands::mesh_llm::resolved_relay_mesh_mode(relay_url).await; - let closed_roster = if matches!(&mode, Ok(mode) if !mode.is_open()) { - Some(crate::commands::mesh_llm::resolve_closed_trusted_owner_ids_at(state, relay_url).await) - } else { - None - }; - classify_open_runtime_observation(mode, closed_roster) -} - -#[derive(Debug, PartialEq, Eq)] -enum SelfOnlyModeAction { - Keep, - AwaitConfirm, - RestartProcess, -} - -fn reconcile_self_only_mode( - state: &mut ModeReconcileState, - query: &Result>, String>, -) -> SelfOnlyModeAction { - match query { - Ok(None) => match mode_reconcile_action(state, Ok(ModeEvidence::Open), false) { - ModeReconcileAction::Keep => SelfOnlyModeAction::Keep, - ModeReconcileAction::AwaitConfirm => SelfOnlyModeAction::AwaitConfirm, - ModeReconcileAction::RestartProcess => SelfOnlyModeAction::RestartProcess, - }, - Err(_) => { - let _ = mode_reconcile_action(state, Err(()), false); - SelfOnlyModeAction::Keep - } - Ok(Some(_)) => { - let _ = mode_reconcile_action(state, Ok(ModeEvidence::Closed), false); - SelfOnlyModeAction::Keep - } - } -} - async fn reconcile_roster( app: &AppHandle, pending_shrink: &mut Option>, - mode_reconcile: &mut ModeReconcileState, ) -> Result<(), String> { - if mode_reconcile.restart_requested() { - return Ok(()); - } let state = app.state::(); let current_request = { let runtime = state.mesh_llm_runtime.lock().await; @@ -385,68 +281,11 @@ async fn reconcile_roster( Some(runtime) => runtime.start_request().clone(), None => { *pending_shrink = None; - *mode_reconcile = ModeReconcileState::default(); return Ok(()); } } }; - let current_owners = current_request.trusted_owner_ids.as_ref(); - // Closed runtimes keep the existing roster-only reconciliation path. Open - // runtimes re-probe NIP-11 so a later open -> closed relay restart is not - // hidden by a permanent mode cache. - let relay_url = current_request - .relay_url - .as_deref() - .map(str::to_owned) - .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); - let Some(current_owners) = current_owners else { - let observation = observe_open_runtime_relay(&state, &relay_url).await; - let evidence = match &observation { - OpenRuntimeObservation::Open => Ok(ModeEvidence::Open), - OpenRuntimeObservation::ClosedWithRoster - | OpenRuntimeObservation::ClosedWithoutRoster(_) => Ok(ModeEvidence::Closed), - OpenRuntimeObservation::Unknown(_) => Err(()), - }; - match mode_reconcile_action(mode_reconcile, evidence, true) { - ModeReconcileAction::Keep => { - if let OpenRuntimeObservation::Unknown(error) = observation { - eprintln!( - "buzz-mesh: open-relay mode recheck failed once; keeping the running policy pending one retry: {error}" - ); - } - } - ModeReconcileAction::AwaitConfirm => eprintln!( - "buzz-mesh: relay membership mode changed; awaiting confirmation before restarting Buzz" - ), - ModeReconcileAction::RestartProcess => { - let reason = match &observation { - OpenRuntimeObservation::ClosedWithRoster => "open-to-closed", - OpenRuntimeObservation::ClosedWithoutRoster(_) => { - "open-to-closed-without-roster" - } - OpenRuntimeObservation::Unknown(_) => "open-mode-unknown", - OpenRuntimeObservation::Open => return Ok(()), - }; - if !request_mode_restart(app, mode_reconcile, reason)? { - eprintln!( - "buzz-mesh: mode-policy restart is already pending or throttled after a recent attempt; keeping the running policy" - ); - } else { - match &observation { - OpenRuntimeObservation::ClosedWithRoster => eprintln!( - "buzz-mesh: relay repeatedly reports membership enforcement; restarting Buzz to apply its allowlist" - ), - OpenRuntimeObservation::ClosedWithoutRoster(error) => eprintln!( - "buzz-mesh: relay repeatedly reports membership enforcement but its roster is unavailable; restarting Buzz fail-closed: {error}" - ), - OpenRuntimeObservation::Unknown(error) => eprintln!( - "buzz-mesh: open-relay mode remained unknown across two polls; restarting Buzz fail-closed: {error}" - ), - OpenRuntimeObservation::Open => {} - } - } - } - } + let Some(current_owners) = current_request.trusted_owner_ids.as_ref() else { *pending_shrink = None; return Ok(()); }; @@ -455,42 +294,12 @@ 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 = if runtime_needs_mode_probe(current_owners) { - // Self-only is the fail-safe startup policy when the initial NIP-11 - // probe is inconclusive. Re-probe this isolated state so a later valid - // open result can recover it; errors still keep it self-only. - crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await - } else { - crate::commands::mesh_llm::resolve_closed_trusted_owner_ids_at(&state, &relay_url) - .await - .map(Some) - }; - if matches!(query, Ok(Some(_))) { - // A successful closed-roster read is decisive non-restarting mode - // evidence, so a prior cross-process mode-restart throttle may clear. - clear_mode_restart(app); - } - if current_owners.is_empty() { - match reconcile_self_only_mode(mode_reconcile, &query) { - SelfOnlyModeAction::Keep => {} - SelfOnlyModeAction::AwaitConfirm => { - eprintln!( - "buzz-mesh: isolated startup fallback observed an open relay; awaiting confirmation before restarting Buzz" - ); - return Ok(()); - } - SelfOnlyModeAction::RestartProcess => { - if !request_mode_restart(app, mode_reconcile, "self-only-to-open")? { - eprintln!( - "buzz-mesh: open-mode recovery restart is already pending or throttled after a recent attempt; keeping the self-only fallback" - ); - } - return Ok(()); - } - } - } else { - *mode_reconcile = ModeReconcileState::default(); - } + let relay_url = current_request + .relay_url + .as_deref() + .map(str::to_owned) + .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state)); + let query = crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await; match roster_reconcile_action(current_owners, pending_shrink.as_deref(), query) { RosterReconcileAction::Keep => { *pending_shrink = None; @@ -749,44 +558,10 @@ mod tests { ); } - #[test] - fn self_only_wiring_never_restarts_after_one_open_observation() { - let mut state = ModeReconcileState::default(); - let query = Ok(None); - assert_eq!( - reconcile_self_only_mode(&mut state, &query), - SelfOnlyModeAction::AwaitConfirm - ); - } - - #[test] - fn self_only_wiring_restarts_after_two_open_observations() { - let mut state = ModeReconcileState::default(); - let query = Ok(None); - assert_eq!( - reconcile_self_only_mode(&mut state, &query), - SelfOnlyModeAction::AwaitConfirm - ); - assert_eq!( - reconcile_self_only_mode(&mut state, &query), - SelfOnlyModeAction::RestartProcess - ); - } - - // The other side of that rule: an allowlist that admits OTHER owners is - // never relaxed on the strength of a NIP-11 read. A relay downgrade (or a - // spoofed document) must not de-restrict a running mesh. - #[test] - fn open_relay_report_keeps_a_multi_party_allowlist() { - let current = vec!["owner-a".to_string(), "owner-b".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(None)); - assert_eq!(action, RosterReconcileAction::Keep); - } - #[test] fn unchanged_roster_is_a_noop() { let current = vec!["owner-a".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(Some(vec!["owner-a".to_string()]))); + let action = roster_reconcile_action(¤t, None, Ok(vec!["owner-a".to_string()])); assert_eq!(action, RosterReconcileAction::Keep); } @@ -795,7 +570,7 @@ mod tests { fn roster_growth_requests_process_restart_immediately() { let current = vec!["owner-a".to_string()]; let fresh = vec!["owner-a".to_string(), "owner-c".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(Some(fresh))); + let action = roster_reconcile_action(¤t, None, Ok(fresh)); assert_eq!(action, RosterReconcileAction::RestartProcess); } @@ -804,7 +579,7 @@ mod tests { fn roster_shrink_awaits_confirmation_first() { let current = vec!["owner-a".to_string(), "owner-b".to_string()]; let reduced = vec!["owner-a".to_string()]; - let action = roster_reconcile_action(¤t, None, Ok(Some(reduced.clone()))); + let action = roster_reconcile_action(¤t, None, Ok(reduced.clone())); assert_eq!(action, RosterReconcileAction::AwaitConfirm(reduced)); } @@ -813,7 +588,7 @@ mod tests { fn roster_shrink_requests_process_restart_once_confirmed() { let current = vec!["owner-a".to_string(), "owner-b".to_string()]; let reduced = vec!["owner-a".to_string()]; - let action = roster_reconcile_action(¤t, Some(&reduced), Ok(Some(reduced.clone()))); + let action = roster_reconcile_action(¤t, Some(&reduced), Ok(reduced.clone())); assert_eq!(action, RosterReconcileAction::RestartProcess); } @@ -824,11 +599,8 @@ mod tests { let current = vec!["a".to_string(), "b".to_string(), "c".to_string()]; let first_reduced = vec!["a".to_string(), "b".to_string()]; let second_reduced = vec!["a".to_string()]; - let action = roster_reconcile_action( - ¤t, - Some(&first_reduced), - Ok(Some(second_reduced.clone())), - ); + let action = + roster_reconcile_action(¤t, Some(&first_reduced), Ok(second_reduced.clone())); assert_eq!(action, RosterReconcileAction::AwaitConfirm(second_reduced)); } @@ -837,10 +609,10 @@ mod tests { #[test] fn genuinely_empty_roster_awaits_then_restarts_to_self_only() { let current = vec!["owner-a".to_string()]; - let first = roster_reconcile_action(¤t, None, Ok(Some(Vec::new()))); + let first = roster_reconcile_action(¤t, None, Ok(Vec::new())); assert_eq!(first, RosterReconcileAction::AwaitConfirm(Vec::new())); let empty: Vec = Vec::new(); - let confirmed = roster_reconcile_action(¤t, Some(&empty), Ok(Some(Vec::new()))); + let confirmed = roster_reconcile_action(¤t, Some(&empty), Ok(Vec::new())); assert_eq!(confirmed, RosterReconcileAction::RestartProcess); } @@ -849,35 +621,12 @@ mod tests { fn roster_shrink_then_recovery_keeps_allowlist() { let current = vec!["owner-a".to_string(), "owner-b".to_string()]; let reduced = vec!["owner-a".to_string()]; - let held = roster_reconcile_action(¤t, None, Ok(Some(reduced.clone()))); + let held = roster_reconcile_action(¤t, None, Ok(reduced.clone())); assert_eq!(held, RosterReconcileAction::AwaitConfirm(reduced.clone())); - let recovered = - roster_reconcile_action(¤t, Some(&reduced), Ok(Some(current.clone()))); + let recovered = roster_reconcile_action(¤t, Some(&reduced), Ok(current.clone())); assert_eq!(recovered, RosterReconcileAction::Keep); } - #[test] - fn only_self_only_closed_runtimes_reprobe_mode() { - let self_only: Vec = Vec::new(); - let closed = vec!["owner-a".to_string()]; - assert!(runtime_needs_mode_probe(&self_only)); - assert!(!runtime_needs_mode_probe(&closed)); - } - - #[test] - fn open_to_closed_with_unavailable_roster_fails_closed() { - let observation = classify_open_runtime_observation( - Ok(crate::mesh_llm::MeshRelayMode::ClosedMembershipEnforced), - Some(Err("relay returned no membership snapshot".to_string())), - ); - assert_eq!( - observation, - OpenRuntimeObservation::ClosedWithoutRoster( - "relay returned no membership snapshot".to_string() - ) - ); - } - #[test] fn member_heartbeat_leaves_room_before_admission_status_expires() { assert!( diff --git a/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs b/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs deleted file mode 100644 index 54d8ccc77be..00000000000 --- a/desktop/src-tauri/src/mesh_llm/coordinator_admission.rs +++ /dev/null @@ -1,228 +0,0 @@ -use std::time::Duration; - -use tauri::{AppHandle, Manager}; - -/// Prevent a flaky mode probe from creating an unbounded whole-app restart -/// loop while changing admission policy. -const MODE_RESTART_COOLDOWN: Duration = Duration::from_secs(10 * 60); -const MODE_RESTART_MARKER: &str = "mesh-mode-recovery-restart"; - -fn mode_restart_marker(app: &AppHandle) -> Result { - Ok(app - .path() - .app_data_dir() - .map_err(|error| format!("failed to resolve app data directory: {error}"))? - .join(MODE_RESTART_MARKER)) -} - -fn restart_marker_is_throttled( - modified: std::time::SystemTime, - now: std::time::SystemTime, -) -> bool { - // A clock rollback makes the marker appear to be in the future. Fail safe: - // retain the throttle rather than disabling the only cross-process bound. - now.duration_since(modified) - .map_or(true, |elapsed| elapsed < MODE_RESTART_COOLDOWN) -} - -fn mode_restart_is_throttled(app: &AppHandle) -> Result { - let marker = mode_restart_marker(app)?; - let modified = match std::fs::metadata(marker).and_then(|metadata| metadata.modified()) { - Ok(modified) => modified, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), - Err(error) => return Err(format!("failed to read mesh restart marker: {error}")), - }; - Ok(restart_marker_is_throttled( - modified, - std::time::SystemTime::now(), - )) -} - -fn mark_mode_restart(app: &AppHandle, reason: &str) -> Result<(), String> { - let marker = mode_restart_marker(app)?; - if let Some(parent) = marker.parent() { - std::fs::create_dir_all(parent) - .map_err(|error| format!("failed to create app data directory: {error}"))?; - } - std::fs::write(marker, format!("{reason}\n")) - .map_err(|error| format!("failed to write mesh restart marker: {error}")) -} - -pub(super) fn clear_mode_restart(app: &AppHandle) { - if let Ok(marker) = mode_restart_marker(app) { - match std::fs::remove_file(marker) { - Ok(()) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => eprintln!("buzz-mesh: failed to clear mesh restart marker: {error}"), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum ModeEvidence { - Open, - Closed, -} - -#[derive(Debug, Default, PartialEq, Eq)] -pub(super) struct ModeReconcileState { - pending: Option, - consecutive_unknown: u8, - restart_requested: bool, -} - -impl ModeReconcileState { - pub(super) fn restart_requested(&self) -> bool { - self.restart_requested - } - - pub(super) fn latch_restart(&mut self) -> bool { - if self.restart_requested { - false - } else { - self.restart_requested = true; - true - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub(super) enum ModeReconcileAction { - Keep, - AwaitConfirm, - RestartProcess, -} - -pub(super) fn request_mode_restart( - app: &AppHandle, - state: &mut ModeReconcileState, - reason: &str, -) -> Result { - if state.restart_requested || mode_restart_is_throttled(app)? { - return Ok(false); - } - mark_mode_restart(app, reason)?; - if !state.latch_restart() { - return Ok(false); - } - app.request_restart(); - Ok(true) -} - -/// Require two consecutive mode observations before restarting the whole app. -/// Unknown observations preserve a running policy once, but a second -/// consecutive unknown while admission is unenforced restarts fail-closed. -pub(super) fn mode_reconcile_action( - state: &mut ModeReconcileState, - evidence: Result, - running_open: bool, -) -> ModeReconcileAction { - match evidence { - Err(()) => { - state.pending = None; - state.consecutive_unknown = state.consecutive_unknown.saturating_add(1); - if running_open && state.consecutive_unknown >= 2 { - state.consecutive_unknown = 0; - ModeReconcileAction::RestartProcess - } else { - ModeReconcileAction::Keep - } - } - Ok(observed) => { - state.consecutive_unknown = 0; - let transition = (running_open && observed == ModeEvidence::Closed) - || (!running_open && observed == ModeEvidence::Open); - if !transition { - state.pending = None; - return ModeReconcileAction::Keep; - } - if state.pending == Some(observed) { - state.pending = None; - ModeReconcileAction::RestartProcess - } else { - state.pending = Some(observed); - ModeReconcileAction::AwaitConfirm - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn restart_latch_only_arms_once() { - let mut state = ModeReconcileState::default(); - assert!(state.latch_restart()); - assert!(state.restart_requested()); - assert!(!state.latch_restart()); - } - - #[test] - fn clock_rollback_keeps_restart_throttled() { - let now = std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(100); - let future_marker = now + Duration::from_secs(1); - assert!(restart_marker_is_throttled(future_marker, now)); - } - - #[test] - fn old_restart_marker_is_not_throttled() { - let marker = std::time::SystemTime::UNIX_EPOCH; - let now = marker + MODE_RESTART_COOLDOWN + Duration::from_secs(1); - assert!(!restart_marker_is_throttled(marker, now)); - } - - #[test] - fn transition_requires_two_matching_observations() { - let mut state = ModeReconcileState::default(); - assert_eq!( - mode_reconcile_action(&mut state, Ok(ModeEvidence::Closed), true), - ModeReconcileAction::AwaitConfirm - ); - assert_eq!( - mode_reconcile_action(&mut state, Ok(ModeEvidence::Closed), true), - ModeReconcileAction::RestartProcess - ); - } - - #[test] - fn recovery_cancels_pending_restart() { - let mut state = ModeReconcileState::default(); - assert_eq!( - mode_reconcile_action(&mut state, Ok(ModeEvidence::Closed), true), - ModeReconcileAction::AwaitConfirm - ); - assert_eq!( - mode_reconcile_action(&mut state, Ok(ModeEvidence::Open), true), - ModeReconcileAction::Keep - ); - assert_eq!(state, ModeReconcileState::default()); - } - - #[test] - fn persistently_unknown_open_runtime_restarts_fail_closed() { - let mut state = ModeReconcileState::default(); - assert_eq!( - mode_reconcile_action(&mut state, Err(()), true), - ModeReconcileAction::Keep - ); - assert_eq!( - mode_reconcile_action(&mut state, Err(()), true), - ModeReconcileAction::RestartProcess - ); - } - - #[test] - fn isolated_open_recovery_requires_confirmation() { - let mut state = ModeReconcileState::default(); - assert_eq!( - mode_reconcile_action(&mut state, Ok(ModeEvidence::Open), false), - ModeReconcileAction::AwaitConfirm - ); - assert_eq!( - mode_reconcile_action(&mut state, Ok(ModeEvidence::Open), false), - ModeReconcileAction::RestartProcess - ); - } -} diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index d347a16d806..2df4a7f8c5e 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -20,66 +20,6 @@ fn status_is_fresh(event: &nostr::Event, now: u64) -> bool { >= now } -/// Whether the relay enforces NIP-43 relay membership. -/// -/// This is a property of the *relay deployment*, not of any particular query -/// result, and it is resolved from the relay's NIP-11 document (NIP-43 is -/// advertised only when membership is actually enforced). It must never be -/// inferred from an absent membership snapshot: on a closed relay a snapshot -/// can also be missing because of a transient replication gap, and treating -/// that as "open" would silently drop admission enforcement exactly when the -/// relay is least healthy. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MeshRelayMode { - /// Membership is enforced (`BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`). Every - /// participant is a NIP-43 direct member, so the membership snapshot is the - /// trust boundary for both admission and routing. - ClosedMembershipEnforced, - /// Membership is not enforced. Signing in never writes a `relay_members` - /// row, so the snapshot lists at most whoever was added out of band and is - /// structurally useless as a trust boundary. Mesh runs unenforced (stock - /// MeshLLM behaviour) and is scoped by the relay-derived mesh topic. - OpenNoMembership, -} - -impl MeshRelayMode { - pub(crate) fn is_open(self) -> bool { - matches!(self, Self::OpenNoMembership) - } -} - -/// Resolve [`MeshRelayMode`] from a relay's NIP-11 document. -/// -/// `buzz-relay` lists NIP-43 in `supported_nips` only when it has a stable -/// signing key *and* `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, so the advertisement -/// is a faithful proxy for "membership is enforced here" when the field is -/// well formed. A missing or malformed `supported_nips` field is rejected so -/// callers keep the fail-closed fallback rather than treating an ambiguous -/// response as evidence that membership is disabled. -pub fn relay_mesh_mode_from_nip11(document: &serde_json::Value) -> Result { - let nips = document - .get("supported_nips") - .and_then(serde_json::Value::as_array) - .ok_or_else(|| "NIP-11 document has no supported_nips array".to_string())?; - let nips = nips - .iter() - .enumerate() - .map(|(index, nip)| { - nip.as_u64() - .filter(|value| *value <= u32::MAX as u64) - .map(|value| value as u32) - .ok_or_else(|| { - format!("NIP-11 supported_nips[{index}] is not a valid u32 NIP number") - }) - }) - .collect::, _>>()?; - if nips.contains(&43) { - Ok(MeshRelayMode::ClosedMembershipEnforced) - } else { - Ok(MeshRelayMode::OpenNoMembership) - } -} - fn dedupe_targets(targets: Vec) -> Vec { let mut by_endpoint_and_model = BTreeMap::<(String, String), MeshServeTarget>::new(); for target in targets { @@ -232,47 +172,14 @@ fn endpoint_binding_is_valid(event: &nostr::Event, content: &serde_json::Value) .is_ok() } -#[cfg(test)] pub fn availability_from_events(events: Vec) -> MeshAvailability { - availability_from_events_for_mode(events, MeshRelayMode::ClosedMembershipEnforced) -} - -/// Resolve availability from relay events, scoped to the relay's membership mode. -/// -/// On a membership-enforcing relay the NIP-43 snapshot gates routing: a serve -/// target is selectable only when its status note is signed by a current -/// member. On a relay that does not enforce membership there is no such -/// roster to intersect with — requiring one would make every serving peer -/// permanently undiscoverable — so status notes are accepted on their own -/// merits (fresh, well-formed, and carrying a valid owner-signed endpoint -/// binding). The mesh topic is derived from the relay origin, so this widens -/// discovery to that relay's participants, not to the public internet. -pub fn availability_from_events_for_mode( - events: Vec, - mode: MeshRelayMode, -) -> MeshAvailability { if events.is_empty() { return MeshAvailability::unavailable("Buzz shared compute status is not published yet"); } - // On an open relay the snapshot is not a trust boundary even when one - // happens to exist (added out of band): admission runs `TrustPolicy::Off` - // there, so intersecting routing with a stray roster would hide peers the - // mesh happily admits. Ignore membership entirely and accept status notes - // on their own merits. - let members = if mode.is_open() { - None - } else { - match latest_membership_list(&events) { - Some(members) => Some(members), - // An enforcing relay always publishes a snapshot (even a - // zero-member one), so its absence means an incomplete read: - // stay closed. - None => { - return MeshAvailability::unavailable( - "Buzz shared compute is waiting for the current member roster", - ); - } - } + let Some(members) = latest_membership_list(&events) else { + return MeshAvailability::unavailable( + "Buzz shared compute is waiting for the current member roster", + ); }; // Status is replaceable per member pubkey, so a query returns multiple @@ -284,14 +191,12 @@ pub fn availability_from_events_for_mode( let now = nostr::Timestamp::now().as_secs(); for event in events { - if event.kind.as_u16() as u64 != MESH_STATUS_KIND || !status_is_fresh(&event, now) { + if event.kind.as_u16() as u64 != MESH_STATUS_KIND + || !status_is_fresh(&event, now) + || !members.contains(&event.pubkey.to_hex().to_ascii_lowercase()) + { continue; } - if let Some(members) = members.as_ref() { - if !members.contains(&event.pubkey.to_hex().to_ascii_lowercase()) { - continue; - } - } let Ok(content) = serde_json::from_str::(&event.content) else { continue; }; diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index 1aa7ead858e..e206c53886a 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -1,16 +1,12 @@ use std::collections::BTreeMap; mod coordinator; -mod coordinator_admission; pub(crate) use coordinator::{publish_current_status_once, publish_stopped_status_once_at}; pub use coordinator::{start_coordinator, MeshCoordinator, KIND_BUZZ_MESH_MEMBER_STATUS}; mod discovery; -#[cfg(test)] -pub use discovery::availability_from_events; pub use discovery::{ - availability_from_events_for_mode, mesh_status_filter, owner_ids_from_events, - relay_membership_filter, relay_mesh_mode_from_nip11, MeshRelayMode, + availability_from_events, mesh_status_filter, owner_ids_from_events, relay_membership_filter, }; pub(crate) use discovery::{ current_member_pubkeys, has_membership_snapshot, MESH_STATUS_PAGE_SIZE, diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index a1db42225f3..557cd040fab 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -665,176 +665,6 @@ fn owner_roster_without_membership_list_fails_closed() { assert!(super::owner_ids_from_events(&events).is_empty()); } -#[test] -fn relay_mesh_mode_reads_nip43_advertisement() { - use super::MeshRelayMode; - - // `buzz-relay` advertises NIP-43 only when membership is enforced, so the - // advertisement is the deployment-mode signal. - assert_eq!( - super::relay_mesh_mode_from_nip11(&json!({"supported_nips": [1, 11, 42, 43]})), - Ok(MeshRelayMode::ClosedMembershipEnforced) - ); - // The live open relay's actual shape (no 43). - assert_eq!( - super::relay_mesh_mode_from_nip11( - &json!({"supported_nips": [1, 2, 10, 11, 16, 17, 23, 25, 29, 33, 38, 42, 50, 56]}) - ), - Ok(MeshRelayMode::OpenNoMembership) - ); - // Missing, malformed, or partially malformed mode evidence must fail at - // the caller. An ambiguous NIP-11 response is not proof of either mode. - assert!(super::relay_mesh_mode_from_nip11(&json!({})).is_err()); - assert!(super::relay_mesh_mode_from_nip11(&json!({"supported_nips": "not-an-array"})).is_err()); - for malformed in [ - json!({"supported_nips": [1, "43"]}), - json!({"supported_nips": [1, {"nip": 43}]}), - json!({"supported_nips": [1, -1]}), - json!({"supported_nips": [1, 43.5]}), - json!({"supported_nips": [1, 4_294_967_296_u64]}), - ] { - assert!( - super::relay_mesh_mode_from_nip11(&malformed).is_err(), - "partially malformed supported_nips must not prove a relay mode: {malformed}" - ); - } -} - -#[test] -fn open_relay_routes_without_a_membership_snapshot() { - use super::MeshRelayMode; - - // The bug this fixes: on a relay that does not enforce membership, no - // kind:13534 snapshot ever lists ordinary users, so requiring one made - // every serving peer permanently undiscoverable — each node sat at - // "1 NODE" with an empty peer list forever. - let endpoint = test_endpoint_token(); - let events = vec![signed_reporter_target( - &"a".repeat(64), - "model-open", - &endpoint, - )]; - - let closed = super::availability_from_events_for_mode( - events.clone(), - MeshRelayMode::ClosedMembershipEnforced, - ); - assert!( - closed.serve_targets.is_empty(), - "an enforcing relay must still wait for its roster" - ); - - let open = super::availability_from_events_for_mode(events, MeshRelayMode::OpenNoMembership); - assert_eq!(open.serve_targets.len(), 1); - assert_eq!(open.serve_targets[0].endpoint_addr, endpoint); -} - -#[test] -fn open_relay_ignores_a_stray_membership_snapshot() { - use super::MeshRelayMode; - - // On an open relay a kind:13534 snapshot can still exist (members added - // out of band), but it is not a trust boundary there: admission runs - // `TrustPolicy::Off` regardless. Intersecting routing with that stray - // roster would hide peers the mesh happily admits — a hybrid state where - // admission and discovery disagree. Routing must ignore it entirely. - let reporter_secret = "a".repeat(64); - let reporter_pubkey = nostr::Keys::parse(&reporter_secret) - .unwrap() - .public_key() - .to_hex(); - let endpoint = test_endpoint_token(); - let events = vec![ - // Snapshot deliberately lists someone else, NOT the reporter. - signed_membership_event(&["f".repeat(64)]), - signed_reporter_target(&reporter_secret, "model-open", &endpoint), - ]; - - let closed = super::availability_from_events_for_mode( - events.clone(), - MeshRelayMode::ClosedMembershipEnforced, - ); - assert!( - closed.serve_targets.is_empty(), - "an enforcing relay still intersects routing with the roster" - ); - - let open = super::availability_from_events_for_mode(events, MeshRelayMode::OpenNoMembership); - assert_eq!( - open.serve_targets.len(), - 1, - "an open relay accepts a non-roster reporter's status: {reporter_pubkey} was hidden" - ); - assert_eq!(open.serve_targets[0].endpoint_addr, endpoint); -} - -#[test] -fn closed_relay_without_snapshot_never_silently_opens_up() { - use super::MeshRelayMode; - - // Regression guard for the risk this change introduces. On an enforcing - // relay a missing snapshot is a transient read gap, NOT permission to - // admit everyone: if that ever resolved to the open path, one replication - // blip would drop admission enforcement on a closed relay. - let events = vec![ - signed_reporter_target(&"b".repeat(64), "model-a", &test_endpoint_token()), - signed_reporter_target(&"c".repeat(64), "model-b", &test_endpoint_token()), - ]; - - let availability = - super::availability_from_events_for_mode(events, MeshRelayMode::ClosedMembershipEnforced); - assert!(availability.serve_targets.is_empty()); - assert_eq!( - availability.reason.as_deref(), - Some("Buzz shared compute is waiting for the current member roster") - ); -} - -#[test] -fn open_relay_still_enforces_freshness_and_endpoint_binding() { - use super::MeshRelayMode; - - // Dropping the membership intersection must not drop the other - // protections: a stale note is still unroutable, and an unsigned endpoint - // substitution is still rejected. - let stale = signed_reporter_target_at( - &"d".repeat(64), - "stale-model", - &test_endpoint_token(), - 1_000, - ); - let stale_availability = - super::availability_from_events_for_mode(vec![stale], MeshRelayMode::OpenNoMembership); - assert!( - stale_availability.serve_targets.is_empty(), - "freshness still gates routing on an open relay" - ); - - let signed_endpoint = test_endpoint_token(); - let mut forged = serde_json::from_str::( - &signed_reporter_target(&"e".repeat(64), "model-x", &signed_endpoint).content, - ) - .expect("status content is json"); - // Swap in an endpoint the owner never signed for. Assert the substitution - // is real so this test can never pass vacuously by reusing the same token. - let substituted_endpoint = test_endpoint_token(); - assert_ne!(signed_endpoint, substituted_endpoint); - forged["serveTargets"][0]["endpointAddr"] = json!(substituted_endpoint); - let keys = nostr::Keys::parse(&"e".repeat(64)).unwrap(); - let substituted = super::coordinator::build_status_report_event(forged) - .expect("status builder") - .sign_with_keys(&keys) - .expect("signs"); - let forged_availability = super::availability_from_events_for_mode( - vec![substituted], - MeshRelayMode::OpenNoMembership, - ); - assert!( - forged_availability.serve_targets.is_empty(), - "endpoint binding is still verified on an open relay" - ); -} - #[test] fn serving_usage_extracts_local_and_remote_attempts() { // Captured shape from a live serving node's status payload. The local vs From d30959c3f8ac88664013a95e4b0acb096a38643a Mon Sep 17 00:00:00 2001 From: Alessandro Joabar Date: Fri, 28 Aug 2026 21:16:43 -0400 Subject: [PATCH 19/20] Refine shared compute setup experience Signed-off-by: Alessandro Joabar --- desktop/src-tauri/src/commands/mesh_llm.rs | 26 +- .../src-tauri/src/commands/mesh_snapshot.rs | 45 ++ desktop/src-tauri/src/commands/mod.rs | 4 + desktop/src-tauri/src/lib.rs | 1 + desktop/src-tauri/src/mesh_llm/discovery.rs | 8 +- desktop/src-tauri/src/mesh_llm/mod.rs | 3 + desktop/src-tauri/src/mesh_llm/snapshot.rs | 352 ++++++++++++++ desktop/src-tauri/src/mesh_llm_stubs.rs | 5 + desktop/src/features/agents/AGENTS.md | 2 + .../features/agents/openCreateAgentEvent.ts | 1 + .../agents/ui/RequestedAgentCreateDialogs.tsx | 17 + .../mesh-compute/communityComputeFixtures.ts | 234 +++++++++ .../communityComputeHexLayout.test.mjs | 358 ++++++++++++++ .../mesh-compute/communityComputeHexLayout.ts | 448 ++++++++++++++++++ .../communityComputeMapModel.test.mjs | 206 ++++++++ .../mesh-compute/communityComputeMapModel.ts | 365 ++++++++++++++ .../mesh-compute/hooks/useMeshNodeStatus.ts | 32 +- .../mesh-compute/hooks/useMeshSnapshot.ts | 62 +++ .../ui/CommunityComputeTerritoryMap.tsx | 342 +++++++++++++ .../ui/MeshComputeSettingsCard.tsx | 257 ++++++---- desktop/src/shared/api/tauriMesh.ts | 32 ++ .../src/shared/styles/globals/animations.css | 11 + 22 files changed, 2704 insertions(+), 107 deletions(-) create mode 100644 desktop/src-tauri/src/commands/mesh_snapshot.rs create mode 100644 desktop/src-tauri/src/mesh_llm/snapshot.rs create mode 100644 desktop/src/features/mesh-compute/communityComputeFixtures.ts create mode 100644 desktop/src/features/mesh-compute/communityComputeHexLayout.test.mjs create mode 100644 desktop/src/features/mesh-compute/communityComputeHexLayout.ts create mode 100644 desktop/src/features/mesh-compute/communityComputeMapModel.test.mjs create mode 100644 desktop/src/features/mesh-compute/communityComputeMapModel.ts create mode 100644 desktop/src/features/mesh-compute/hooks/useMeshSnapshot.ts create mode 100644 desktop/src/features/mesh-compute/ui/CommunityComputeTerritoryMap.tsx diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 7356cd7fc0c..a4f5863509d 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -176,7 +176,7 @@ fn advance_mesh_status_cursor( Ok(cursor) } -async fn query_mesh_discovery_events_at( +pub(super) async fn query_mesh_discovery_events_at( state: &AppState, relay_url: &str, ) -> Result, String> { @@ -484,7 +484,7 @@ pub async fn mesh_start_node( let started = mesh_llm::DesktopMeshRuntime::start(request) .await .map_err(|error| format!("{error:#}"))?; - let status = match started.status().await { + let mut status = match started.status().await { Ok(status) => status, Err(error) => { let cleanup = started.stop().await; @@ -520,11 +520,23 @@ pub async fn mesh_start_node( // (leaves a warming node alone) can loop a slow-but-alive node, and an // unstartable config fails earlier in `start()`. Probe is informational. save_mesh_sharing_config(&app, config)?; - if let Err(error) = wait_for_mesh_inference(&config.model_id).await { - eprintln!( - "buzz-mesh: node started but inference is not ready yet ({error}); \ - leaving it to warm up (Share Compute stays armed for next launch)" - ); + match wait_for_mesh_inference(&config.model_id).await { + Ok(()) => { + // The status captured immediately after runtime installation can + // still say `starting`. Re-read after the readiness probe so the + // command response reflects the successful boot the user waited + // for instead of pinning the UI's transition indicator. + let runtime = state.mesh_llm_runtime.lock().await; + if let Some(running) = runtime.as_ref() { + status = running.status().await.map_err(|error| error.to_string())?; + } + } + Err(error) => { + eprintln!( + "buzz-mesh: node started but inference is not ready yet ({error}); \ + leaving it to warm up (Share Compute stays armed for next launch)" + ); + } } } mesh_llm::publish_current_status_once(&app, "start").await; diff --git a/desktop/src-tauri/src/commands/mesh_snapshot.rs b/desktop/src-tauri/src/commands/mesh_snapshot.rs new file mode 100644 index 00000000000..389e228439e --- /dev/null +++ b/desktop/src-tauri/src/commands/mesh_snapshot.rs @@ -0,0 +1,45 @@ +//! `mesh_snapshot` — community-wide shared-compute snapshot for the UI. +//! +//! Separate file from `commands/mesh_llm.rs` deliberately: that module is at +//! the 1000-line file ceiling enforced by `desktop/scripts/check-file-sizes.mjs`. +//! +//! Read-only. Queries the same member-status + membership filters routing uses, +//! then projects them through [`mesh_llm::snapshot_from_events`], which applies +//! the membership/binding/freshness rules. This never selects a serve target — +//! routing stays with `availability_from_events`. + +use tauri::State; + +use crate::app_state::AppState; +use crate::mesh_llm; + +type CmdResult = Result; + +/// Snapshot of who is sharing compute in this community right now. +/// +/// An empty mesh is a normal state, not an error: the snapshot carries a +/// `reason` string instead of failing, so the card can render an honest empty +/// state. A genuine relay/transport failure still returns `Err`. +#[tauri::command] +pub async fn mesh_snapshot(state: State<'_, AppState>) -> CmdResult { + // Reuse discovery's author-scoped, composite-cursor pagination. A direct + // two-filter query stops at the status filter's 100-event page size and + // silently undercounts larger communities. + let relay_url = crate::relay::relay_ws_url_with_override(&state); + let events = super::mesh_llm::query_mesh_discovery_events_at(&state, &relay_url) + .await + .map_err(|error| format!("Shared compute status query failed: {error}"))?; + + // Identify this member's own device so the card can say "including yours". + // A missing/locked identity is not fatal here — the snapshot is still + // useful, just without self-attribution. + let self_pubkey = state + .signing_keys() + .ok() + .map(|keys| keys.public_key().to_hex()); + + Ok(mesh_llm::snapshot_from_events( + events, + self_pubkey.as_deref(), + )) +} diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 7cb2d8e3b83..42da16ad9c0 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -39,6 +39,8 @@ mod media_upload_progress; pub(crate) mod mesh_llm; #[cfg(feature = "mesh-llm")] pub(crate) mod mesh_readiness; +#[cfg(feature = "mesh-llm")] +mod mesh_snapshot; mod messages; mod notifications; mod observer_archive; @@ -100,6 +102,8 @@ pub use media_download::*; pub use media_raw::*; #[cfg(feature = "mesh-llm")] pub use mesh_llm::*; +#[cfg(feature = "mesh-llm")] +pub use mesh_snapshot::*; pub use messages::*; pub use notifications::*; pub use observer_archive::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 2dde312d779..521abd680c0 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -717,6 +717,7 @@ pub fn run() { mesh_serving_usage, mesh_installed_models, mesh_model_catalog, + mesh_snapshot, update_managed_agent, discover_backend_providers, probe_backend_provider, diff --git a/desktop/src-tauri/src/mesh_llm/discovery.rs b/desktop/src-tauri/src/mesh_llm/discovery.rs index 2df4a7f8c5e..8629ed62c90 100644 --- a/desktop/src-tauri/src/mesh_llm/discovery.rs +++ b/desktop/src-tauri/src/mesh_llm/discovery.rs @@ -12,7 +12,7 @@ use super::{dedupe_models, MeshAvailability, MeshModelOption, MeshServeTarget, M pub(super) const STATUS_FRESHNESS_SECS: u64 = 120; pub(crate) const MESH_STATUS_PAGE_SIZE: usize = 100; -fn status_is_fresh(event: &nostr::Event, now: u64) -> bool { +pub(super) fn status_is_fresh(event: &nostr::Event, now: u64) -> bool { event .created_at .as_secs() @@ -63,7 +63,7 @@ pub fn owner_ids_from_events(events: &[nostr::Event]) -> Vec { ids } -fn latest_membership_list(events: &[nostr::Event]) -> Option> { +pub(super) fn latest_membership_list(events: &[nostr::Event]) -> Option> { events .iter() .filter(|event| event.kind.as_u16() == 13_534) @@ -108,7 +108,7 @@ pub(crate) fn has_membership_snapshot(events: &[nostr::Event]) -> bool { events.iter().any(|event| event.kind.as_u16() == 13_534) } -fn owner_id_from_status_event(event: &nostr::Event) -> Option { +pub(super) fn owner_id_from_status_event(event: &nostr::Event) -> Option { let content = serde_json::from_str::(&event.content).ok()?; let owner_id = content .get("ownerId") @@ -136,7 +136,7 @@ fn owner_id_from_status_event(event: &nostr::Event) -> Option { Some(owner_id.to_string()) } -fn endpoint_binding_is_valid(event: &nostr::Event, content: &serde_json::Value) -> bool { +pub(super) fn endpoint_binding_is_valid(event: &nostr::Event, content: &serde_json::Value) -> bool { let Some(endpoint_tokens) = super::identity::advertised_endpoint_tokens(content) else { return false; }; diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index e206c53886a..743f8766d1c 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -13,6 +13,9 @@ pub(crate) use discovery::{ }; use discovery::{device_name_from_status, endpoint_id_from_status, enrich_status_payload_identity}; +mod snapshot; +pub use snapshot::{snapshot_from_events, MeshSnapshot}; + mod catalog; pub(crate) use catalog::canonical_curated_model_id; pub use catalog::{model_catalog, MeshModelCatalog}; diff --git a/desktop/src-tauri/src/mesh_llm/snapshot.rs b/desktop/src-tauri/src/mesh_llm/snapshot.rs new file mode 100644 index 00000000000..b76b4bd12da --- /dev/null +++ b/desktop/src-tauri/src/mesh_llm/snapshot.rs @@ -0,0 +1,352 @@ +//! Community-wide mesh snapshot — an **allowlisted** projection of the member +//! status notes already on the relay. +//! +//! Every sharing member already publishes its whole SDK status payload as +//! kind:30003 content (see `mod.rs::status_report_payload`), but +//! `availability_from_events` reads only `serveTargets`/`models` and discards +//! the rest. This module projects the few extra fields the Share-compute UI +//! needs — per-device shared capacity, a coarse state, and a stable label — +//! and **nothing else**. +//! +//! Deliberately narrow, for two reasons: +//! 1. Routing must keep using [`super::availability_from_events`]. This +//! snapshot is presentation-only and must never select a serve target. +//! 2. The published payload also carries a hardware fingerprint, peer +//! topology, and operational counters. Naming each field we surface keeps +//! the UI from quietly growing into the rest of it. +//! +//! Capacity uses the payload's `my_vram_gb`, which mesh-llm derives from a +//! `vram_bytes` already clamped to the member's own `--max-vram` cap. Raw +//! `gpus[].vram_bytes` is *not* capped, so summing GPUs would advertise more +//! than the member agreed to share. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use super::discovery::{ + device_name_from_status, endpoint_binding_is_valid, endpoint_id_from_status, + latest_membership_list, owner_id_from_status_event, status_is_fresh, STATUS_FRESHNESS_SECS, +}; +use super::{MeshServeTarget, MESH_STATUS_KIND}; + +/// Coarse per-device state. Intentionally not the SDK's `NodeState`: the UI +/// only needs "is this device giving compute, warming up, or just consuming?" +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MeshDeviceState { + /// Advertising at least one routable model. + Serving, + /// Serve-mode, model not ready yet. + Loading, + /// Serve-mode participant with no model work (contributes capacity only). + Standby, + /// Client-mode: consuming someone else's compute, not sharing. + Consuming, +} + +/// One device visible on the mesh. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MeshSnapshotDevice { + /// Stable endpoint/device id, when the note carries one. + pub device_id: Option, + /// Human label: hostname when published, else a short endpoint id. + pub label: String, + /// Shared AI memory in GB, honoring the member's own cap. `None` when the + /// member is not currently running a node (a stopped member publishes only + /// its owner binding). + pub capacity_gb: Option, + /// Models this device is advertising right now. + pub models: Vec, + pub state: MeshDeviceState, + /// Whether this is the local member's own device. + pub is_self: bool, + /// Community member who signed this status note. Safe for profile lookup; + /// endpoint tokens, owner signatures, and raw hardware remain excluded. + pub member_pubkey: Option, + /// Relay event creation time, in Unix seconds. + pub reported_at: Option, + /// Approximate hosted-model footprint reported by MeshLLM, when available. + pub model_size_gb: Option, +} + +/// What the community's shared compute looks like right now. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct MeshSnapshot { + /// Devices advertising at least one routable model. + pub sharing_device_count: usize, + /// Unique current members with at least one ready serving device. + pub contributor_member_count: usize, + /// Summed shared capacity across sharing devices. `None` when no sharing + /// device published a figure — the UI then shows a count with no GB, never + /// a misleading `0 GB`. + pub shared_capacity_gb: Option, + /// Summed model footprint only when every sharing device reports it. + /// `None` preserves mixed-version honesty instead of presenting a partial sum. + pub allocated_capacity_gb: Option, + /// Distinct models ready to use, deduped across devices. + pub models: Vec, + /// Per-device rows, for the topology view. Sharing devices first. + pub devices: Vec, + /// True when the local member's own device is among the sharing devices. + pub includes_self: bool, + /// Members in the community right now (NIP-43 roster size). + /// + /// The denominator for "N of M sharing". Deliberately NOT used to estimate + /// unshared capacity: a member who never starts a node publishes no status + /// note, so their hardware is unknown by design — they never consented to + /// disclose it. Ghost nodes in the topology are a count, never GB. + pub member_count: usize, + /// When this projection was evaluated, in Unix seconds. + pub observed_at: u64, + /// Maximum age of a relay-reported serving status. + pub freshness_seconds: u64, + /// Why the snapshot is empty, when it is. Never a hard error: an empty + /// mesh is a normal, expected state. + pub reason: Option, +} + +impl MeshSnapshot { + fn empty(reason: impl Into) -> Self { + Self { + observed_at: nostr::Timestamp::now().as_secs(), + freshness_seconds: STATUS_FRESHNESS_SECS, + reason: Some(reason.into()), + ..Default::default() + } + } +} + +fn f64_field(content: &serde_json::Value, key: &str) -> Option { + content + .get(key) + .and_then(serde_json::Value::as_f64) + .filter(|value| value.is_finite() && *value > 0.0) +} + +/// Coarse state from the published `node_state`, falling back to whether the +/// note actually advertises routable targets. +fn device_state(content: &serde_json::Value, has_targets: bool) -> MeshDeviceState { + let raw = content + .get("node_state") + .and_then(serde_json::Value::as_str) + .map(str::trim) + .unwrap_or_default() + .to_ascii_lowercase(); + match raw.as_str() { + "serving" => MeshDeviceState::Serving, + "loading" => MeshDeviceState::Loading, + "client" => MeshDeviceState::Consuming, + "standby" => MeshDeviceState::Standby, + // Unknown/absent: trust the routable evidence rather than guessing. + _ if has_targets => MeshDeviceState::Serving, + _ => MeshDeviceState::Standby, + } +} + +/// Project validated member status notes into a presentation snapshot. +/// +/// Applies the same trust rules as routing — current NIP-43 membership, owner +/// binding, endpoint binding, and [`status_is_fresh`] — so a stale or +/// non-member note can never inflate the headline. Pure and total so it can be +/// unit-tested without a relay. +pub fn snapshot_from_events( + events: Vec, + self_member_pubkey: Option<&str>, +) -> MeshSnapshot { + if events.is_empty() { + return MeshSnapshot::empty("No shared compute has been published yet"); + } + let Some(members) = latest_membership_list(&events) else { + return MeshSnapshot::empty("Waiting for the current member roster"); + }; + + let self_pubkey = self_member_pubkey.map(str::to_ascii_lowercase); + let now = nostr::Timestamp::now().as_secs(); + // Status notes are replaceable per owner id, so key on that: two devices + // signed in as the same member each publish their own note. + let mut by_owner = BTreeMap::::new(); + + for event in events { + if event.kind.as_u16() as u64 != MESH_STATUS_KIND + || !status_is_fresh(&event, now) + || !members.contains(&event.pubkey.to_hex().to_ascii_lowercase()) + { + continue; + } + let Ok(content) = serde_json::from_str::(&event.content) else { + continue; + }; + let Some(owner_id) = owner_id_from_status_event(&event) else { + continue; + }; + if !endpoint_binding_is_valid(&event, &content) { + continue; + } + + // Reuse the routing-side target parse so "sharing" here means exactly + // what it means to the router: a validated, advertisable endpoint. + let targets = content + .get("serveTargets") + .or_else(|| content.get("serve_targets")) + .cloned() + .and_then(|value| serde_json::from_value::>(value).ok()) + .unwrap_or_default() + .into_iter() + .filter(|target| { + super::transport_policy::validate_advertised_endpoint(&target.endpoint_addr).is_ok() + }) + .collect::>(); + + let mut models = targets + .iter() + .map(|target| target.model_id.clone()) + .collect::>(); + models.sort(); + models.dedup(); + + let endpoint_id = endpoint_id_from_status(&content, None); + let is_self = self_pubkey + .as_deref() + .is_some_and(|pubkey| event.pubkey.to_hex().to_ascii_lowercase() == pubkey); + + by_owner.insert( + owner_id, + MeshSnapshotDevice { + label: device_name_from_status(&content, endpoint_id.as_deref()) + .unwrap_or_else(|| "Unknown device".to_string()), + device_id: endpoint_id, + capacity_gb: f64_field(&content, "my_vram_gb"), + state: device_state(&content, !models.is_empty()), + models, + is_self, + member_pubkey: Some(event.pubkey.to_hex()), + reported_at: Some(event.created_at.as_secs()), + model_size_gb: f64_field(&content, "model_size_gb"), + }, + ); + } + + let mut devices = by_owner.into_values().collect::>(); + // Sharing devices first, then the local device, then by label so the list + // is stable across polls (no jitter in the topology view). + devices.sort_by(|a, b| { + let sharing = |d: &MeshSnapshotDevice| d.state == MeshDeviceState::Serving; + sharing(b) + .cmp(&sharing(a)) + .then_with(|| b.is_self.cmp(&a.is_self)) + .then_with(|| a.label.cmp(&b.label)) + }); + + let sharing = devices + .iter() + .filter(|device| device.state == MeshDeviceState::Serving) + .collect::>(); + let sharing_device_count = sharing.len(); + let contributor_member_count = sharing + .iter() + .filter_map(|device| device.member_pubkey.as_ref()) + .collect::>() + .len(); + let includes_self = sharing.iter().any(|device| device.is_self); + // Sum only reported figures; keep `None` when nobody reported one so the + // UI can drop the number instead of printing `0 GB`. + let shared_capacity_gb = sharing + .iter() + .filter_map(|device| device.capacity_gb) + .fold(None::, |acc, gb| Some(acc.unwrap_or(0.0) + gb)); + let allocated_capacity_gb = (!sharing.is_empty() + && sharing.iter().all(|device| device.model_size_gb.is_some())) + .then(|| { + sharing + .iter() + .filter_map(|device| device.model_size_gb) + .sum() + }); + let mut models = sharing + .iter() + .flat_map(|device| device.models.iter().cloned()) + .collect::>(); + models.sort(); + models.dedup(); + + let reason = if sharing_device_count == 0 { + Some("No one is sharing compute yet".to_string()) + } else { + None + }; + + MeshSnapshot { + member_count: members.len(), + contributor_member_count, + sharing_device_count, + shared_capacity_gb, + allocated_capacity_gb, + models, + devices, + includes_self, + observed_at: now, + freshness_seconds: STATUS_FRESHNESS_SECS, + reason, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_events_report_a_reason_not_an_error() { + let snapshot = snapshot_from_events(Vec::new(), None); + assert_eq!(snapshot.sharing_device_count, 0); + assert_eq!(snapshot.shared_capacity_gb, None); + assert!(snapshot.reason.is_some()); + assert_eq!(snapshot.freshness_seconds, STATUS_FRESHNESS_SECS); + assert!(snapshot.observed_at > 0); + } + + #[test] + fn missing_membership_snapshot_is_not_treated_as_an_empty_mesh() { + // A relay gap must not read as "nobody is sharing". + let event = nostr::EventBuilder::new(nostr::Kind::Custom(30003), "{}") + .sign_with_keys(&nostr::Keys::generate()) + .expect("test event signs"); + let snapshot = snapshot_from_events(vec![event], None); + assert_eq!( + snapshot.reason.as_deref(), + Some("Waiting for the current member roster") + ); + } + + #[test] + fn unknown_node_state_falls_back_to_routable_evidence() { + assert_eq!( + device_state(&serde_json::json!({}), true), + MeshDeviceState::Serving + ); + assert_eq!( + device_state(&serde_json::json!({}), false), + MeshDeviceState::Standby + ); + assert_eq!( + device_state(&serde_json::json!({"node_state": "client"}), false), + MeshDeviceState::Consuming + ); + } + + #[test] + fn capacity_ignores_zero_and_nonfinite_reports() { + // A node that has not surveyed yet publishes 0.0; that must not count + // as a real reported capacity. + assert_eq!( + f64_field(&serde_json::json!({"my_vram_gb": 0.0}), "my_vram_gb"), + None + ); + assert_eq!( + f64_field(&serde_json::json!({"my_vram_gb": 36.5}), "my_vram_gb"), + Some(36.5) + ); + } +} diff --git a/desktop/src-tauri/src/mesh_llm_stubs.rs b/desktop/src-tauri/src/mesh_llm_stubs.rs index e8c13f48ea9..5b93cf8c766 100644 --- a/desktop/src-tauri/src/mesh_llm_stubs.rs +++ b/desktop/src-tauri/src/mesh_llm_stubs.rs @@ -42,3 +42,8 @@ pub async fn mesh_installed_models( pub async fn mesh_model_catalog() -> CmdResult { Err("mesh-llm feature not enabled".to_string()) } + +#[tauri::command] +pub async fn mesh_snapshot(_state: State<'_, AppState>) -> CmdResult { + Err("mesh-llm feature not enabled".to_string()) +} diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index d9df8c164db..7bc96da683b 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -295,6 +295,8 @@ with a TypeScript lookup table or an id comparison in a component. - Rust: `definition_validation` and inbound persona tests pin the shared Unicode/control-character policy at local, import, publish, and sync gates. +16. **Community-mesh agent creation uses the normal create dialog with a named preset.** The Compute settings CTA requests the `community-mesh` create preset; the app-level create-dialog owner resolves it to Buzz Agent with the `relay-mesh` provider and `auto` model, then passes those values through `AgentDialog`. Keep this as a preset of the canonical create flow—never add a second mesh-specific form or bypass normal runtime catalog validation and persistence. + ## Keep this file true **If you change how agent configuration is modeled, rendered, persisted, diff --git a/desktop/src/features/agents/openCreateAgentEvent.ts b/desktop/src/features/agents/openCreateAgentEvent.ts index 78bf59eb9dc..4504dbfb772 100644 --- a/desktop/src/features/agents/openCreateAgentEvent.ts +++ b/desktop/src/features/agents/openCreateAgentEvent.ts @@ -3,6 +3,7 @@ const OPEN_CREATE_AGENT_EVENT = "buzz:open-create-agent"; export type OpenCreateAgentOptions = { channelId?: string; channelName?: string; + preset?: "community-mesh"; }; let pendingOpenCreateAgent: OpenCreateAgentOptions | null = null; diff --git a/desktop/src/features/agents/ui/RequestedAgentCreateDialogs.tsx b/desktop/src/features/agents/ui/RequestedAgentCreateDialogs.tsx index 4b04de638d7..f63b5987761 100644 --- a/desktop/src/features/agents/ui/RequestedAgentCreateDialogs.tsx +++ b/desktop/src/features/agents/ui/RequestedAgentCreateDialogs.tsx @@ -5,6 +5,7 @@ import { subscribeOpenCreateAgent, type OpenCreateAgentOptions, } from "@/features/agents/openCreateAgentEvent"; +import type { CreatePersonaInput } from "@/shared/api/types"; import { AgentDialog } from "./AgentDialog"; import { usePersonaActions } from "./usePersonaActions"; @@ -16,9 +17,23 @@ export function RequestedAgentCreateDialogs() { name: string; } | null>(null); const [isOpen, setIsOpen] = React.useState(false); + const [initialValues, setInitialValues] = + React.useState(null); const openCreate = React.useEffectEvent((options: OpenCreateAgentOptions) => { personas.prepareCreate(); + setInitialValues( + options.preset === "community-mesh" + ? { + displayName: "Community agent", + avatarUrl: "", + systemPrompt: "", + runtime: "buzz-agent", + provider: "relay-mesh", + model: "auto", + } + : null, + ); setTargetChannel( options.channelId && options.channelName ? { id: options.channelId, name: options.channelName } @@ -42,12 +57,14 @@ export function RequestedAgentCreateDialogs() { ? personas.createPersonaMutation.error : null } + initialValues={initialValues} isDefinitionPending={personas.isPending} mode="definition" onOpenChange={(open) => { if (!open) { setIsOpen(false); setTargetChannel(null); + setInitialValues(null); } }} onSubmitDefinition={(input, intent, backendIntent) => diff --git a/desktop/src/features/mesh-compute/communityComputeFixtures.ts b/desktop/src/features/mesh-compute/communityComputeFixtures.ts new file mode 100644 index 00000000000..e88f6726803 --- /dev/null +++ b/desktop/src/features/mesh-compute/communityComputeFixtures.ts @@ -0,0 +1,234 @@ +import type { + CommunityComputeDeviceInput, + CommunityComputeSnapshotInput, +} from "./communityComputeMapModel"; + +export type CommunityComputeTutorialFixtureId = + | "empty" + | "single-device" + | "small-company" + | "large-dense-500"; + +export type CommunityComputeTutorialFixture = { + id: CommunityComputeTutorialFixtureId; + title: string; + description: string; + snapshot: CommunityComputeSnapshotInput; +}; + +const MODEL_REFS = [ + "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M", + "Qwen/Qwen3.5-35B-A3B-GGUF:Q4_K_M", + "mistralai/Ministral-3-14B-Instruct-GGUF:Q4_K_M", + "bartowski/Llama-3.3-70B-Instruct-GGUF:Q4_K_M", +] as const; + +export const EMPTY_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture = + { + id: "empty", + title: "New community", + description: "No member has shared compute yet.", + snapshot: { + sharingDeviceCount: 0, + contributorMemberCount: 0, + sharedCapacityGb: null, + allocatedCapacityGb: null, + models: [], + devices: [], + includesSelf: false, + memberCount: 8, + reason: "No sharing devices found", + }, + }; + +export const SINGLE_DEVICE_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture = + { + id: "single-device", + title: "First deployment", + description: "One member is sharing a single workstation.", + snapshot: snapshotFromDevices( + [ + device({ + index: 0, + id: "studio-workstation", + label: "Morgan’s Studio", + capacityGb: 24, + modelSizeGb: 18, + isSelf: true, + }), + ], + 1, + ), + }; + +export const SMALL_COMPANY_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture = + { + id: "small-company", + title: "Small company", + description: "A mixed fleet across a small distributed team.", + snapshot: snapshotFromDevices( + [ + ...[ + { capacityGb: 24, modelSizeGb: 18 }, + { capacityGb: 24, modelSizeGb: 18 }, + { capacityGb: 36, modelSizeGb: 28 }, + { capacityGb: 36, modelSizeGb: 28 }, + { capacityGb: 48, modelSizeGb: 36 }, + { capacityGb: 48, modelSizeGb: 36 }, + { capacityGb: 64, modelSizeGb: 48 }, + { capacityGb: 64, modelSizeGb: 48 }, + { capacityGb: 96, modelSizeGb: 72 }, + { capacityGb: 96, modelSizeGb: 72 }, + ].map(({ capacityGb, modelSizeGb }, index) => + device({ + index, + id: `team-macbook-${String(index + 1).padStart(2, "0")}`, + label: `Team MacBook ${String(index + 1).padStart(2, "0")}`, + capacityGb, + modelSizeGb, + isSelf: index === 0, + }), + ), + device({ + index: 10, + id: "intensive-model-host-a", + label: "Intensive model host A", + capacityGb: 768, + modelRef: "unsloth/Kimi-K2-Thinking-GGUF:UD-Q4_K_XL", + modelSizeGb: 646, + }), + device({ + index: 11, + id: "intensive-model-host-b", + label: "Intensive model host B", + capacityGb: 1_440, + modelRef: "unsloth/DeepSeek-V3.2-GGUF:UD-Q4_K_XL", + modelSizeGb: 1_000, + }), + ], + 24, + ), + }; + +/** Deterministic stress fixture using only the production snapshot contract. */ +export const LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE: CommunityComputeTutorialFixture = + { + id: "large-dense-500", + title: "Large company", + description: + "Five hundred deterministic reporting devices for density testing.", + snapshot: createLargeDenseSnapshot(500), + }; + +export const COMMUNITY_COMPUTE_TUTORIAL_FIXTURES: Readonly< + Record +> = { + empty: EMPTY_COMMUNITY_COMPUTE_FIXTURE, + "single-device": SINGLE_DEVICE_COMMUNITY_COMPUTE_FIXTURE, + "small-company": SMALL_COMPANY_COMMUNITY_COMPUTE_FIXTURE, + "large-dense-500": LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE, +}; + +export function createLargeDenseSnapshot( + deviceCount = 500, +): CommunityComputeSnapshotInput { + const count = Math.max(0, Math.floor(deviceCount)); + const capacities = [8, 16, 24, 32, 48, 64, 96, 128] as const; + const devices = Array.from({ length: count }, (_, index) => { + const capacityGb = capacities[index % capacities.length]; + const state = + index % 41 === 0 ? "loading" : index % 17 === 0 ? "standby" : "serving"; + const largeIndex = [49, 149, 249, 349, 449, 499].indexOf(index); + const largeSizes = [170, 230, 350, 480, 700, 1_000] as const; + const largeCapacities = [192, 256, 384, 512, 768, 1_440] as const; + const modelSizeGb = + largeIndex >= 0 + ? largeSizes[largeIndex] + : roundCapacity(capacityGb * 0.72); + return device({ + index, + id: `dense-device-${String(index + 1).padStart(4, "0")}`, + label: + largeIndex >= 0 + ? `${largeCapacities[largeIndex]} GB model host` + : `Compute ${String(index + 1).padStart(4, "0")}`, + capacityGb: largeIndex >= 0 ? largeCapacities[largeIndex] : capacityGb, + modelRef: + largeIndex >= 0 + ? `community/Large-${modelSizeGb}GB-Instruct:FP8` + : undefined, + modelSizeGb, + state, + models: state === "serving" ? undefined : [], + isSelf: index === 0, + }); + }); + return snapshotFromDevices(devices, Math.ceil(count * 1.37)); +} + +type DeviceOptions = { + index: number; + id: string; + label: string; + capacityGb: number; + modelRef?: string; + modelSizeGb?: number; + models?: string[]; + state?: CommunityComputeDeviceInput["state"]; + isSelf?: boolean; +}; + +function device(options: DeviceOptions): CommunityComputeDeviceInput { + const model = + options.modelRef ?? MODEL_REFS[options.index % MODEL_REFS.length]; + return { + deviceId: options.id, + label: options.label, + capacityGb: options.capacityGb, + models: options.models ?? [model], + state: options.state ?? "serving", + isSelf: options.isSelf ?? false, + memberPubkey: `tutorial-member-${String(options.index + 1).padStart(4, "0")}`, + reportedAt: 1_786_032_000, + modelSizeGb: options.modelSizeGb ?? null, + }; +} + +function roundCapacity(value: number): number { + return Math.round(value * 10) / 10; +} + +function snapshotFromDevices( + devices: CommunityComputeDeviceInput[], + memberCount: number, +): CommunityComputeSnapshotInput { + const serving = devices.filter( + (item) => item.state === "serving" && item.models.length > 0, + ); + const models = [...new Set(serving.flatMap((item) => item.models))].sort(); + const sharedCapacityGb = completeSum(serving.map((item) => item.capacityGb)); + const allocatedCapacityGb = completeSum( + serving.map((item) => item.modelSizeGb ?? null), + ); + return { + sharingDeviceCount: serving.length, + contributorMemberCount: new Set( + serving.map((item) => item.memberPubkey ?? item.deviceId), + ).size, + sharedCapacityGb, + allocatedCapacityGb, + models, + devices, + includesSelf: serving.some((item) => item.isSelf), + memberCount, + observedAt: 1_786_032_000, + freshnessSeconds: 120, + reason: serving.length === 0 ? "No sharing devices found" : null, + }; +} + +function completeSum(values: readonly (number | null)[]): number | null { + return values.length > 0 && values.every((value) => value !== null) + ? values.reduce((total, value) => total + (value ?? 0), 0) + : null; +} diff --git a/desktop/src/features/mesh-compute/communityComputeHexLayout.test.mjs b/desktop/src/features/mesh-compute/communityComputeHexLayout.test.mjs new file mode 100644 index 00000000000..ee14e9032df --- /dev/null +++ b/desktop/src/features/mesh-compute/communityComputeHexLayout.test.mjs @@ -0,0 +1,358 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + hexCoordinateKey, + hexDistance, + hexNeighbours, + hexTerritoryBoundaryEdges, + hexTerritoryInteriorEdges, + isContiguousHexTerritory, + layoutCommunityComputeHexTerritories, +} from "./communityComputeHexLayout.ts"; +import { LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE } from "./communityComputeFixtures.ts"; +import { deriveCommunityComputeMapModel } from "./communityComputeMapModel.ts"; + +function assertStructuralInvariants(layout) { + const occupied = new Set(layout.cells.map(hexCoordinateKey)); + assert.equal(occupied.size, layout.cells.length, "cells must not overlap"); + assert.ok( + layout.territories.every((territory) => + isContiguousHexTerritory(territory.cells), + ), + "every territory must be contiguous", + ); + if (layout.cells.length > 0) { + const reached = new Set([hexCoordinateKey(layout.cells[0])]); + const queue = [layout.cells[0]]; + for (let cursor = 0; cursor < queue.length; cursor += 1) { + for (const neighbour of hexNeighbours(queue[cursor])) { + const key = hexCoordinateKey(neighbour); + if (occupied.has(key) && !reached.has(key)) { + reached.add(key); + queue.push(neighbour); + } + } + } + assert.equal( + reached.size, + occupied.size, + "occupied mesh must be connected", + ); + } +} + +function organicGaps(layout) { + const occupied = new Set(layout.cells.map(hexCoordinateKey)); + const empty = new Map(); + for (const cell of layout.cells) { + for (const neighbour of hexNeighbours(cell)) { + const key = hexCoordinateKey(neighbour); + if (!occupied.has(key)) empty.set(key, neighbour); + } + } + return [...empty.values()].filter( + (cell) => + hexNeighbours(cell).filter((neighbour) => + occupied.has(hexCoordinateKey(neighbour)), + ).length >= 2, + ); +} + +function assertBalancedSparseFrontier(layout, deploymentCount) { + const points = layout.cells.map((cell) => ({ + x: Math.sqrt(3) * (cell.q + cell.r / 2), + y: 1.5 * cell.r, + })); + const width = + Math.max(...points.map(({ x }) => x)) - + Math.min(...points.map(({ x }) => x)) + + Math.sqrt(3); + const height = + Math.max(...points.map(({ y }) => y)) - + Math.min(...points.map(({ y }) => y)) + + 2; + const aspectRatio = width / height; + assert.ok( + aspectRatio >= 0.8 && aspectRatio <= 1.3, + `${deploymentCount} deployments have unbalanced aspect ratio ${aspectRatio}`, + ); + + const radius = Math.max( + ...layout.cells.map((cell) => hexDistance(cell, { q: 0, r: 0 })), + ); + const directionalRadii = Array(6).fill(0); + for (const cell of layout.cells) { + const projections = [ + cell.q, + -cell.r, + -cell.q - cell.r, + -cell.q, + cell.r, + cell.q + cell.r, + ]; + for (const [index, projection] of projections.entries()) { + directionalRadii[index] = Math.max(directionalRadii[index], projection); + } + } + assert.ok( + Math.max(...directionalRadii) - Math.min(...directionalRadii) <= 2, + `${deploymentCount} deployments must reach every side evenly`, + ); + + const outerOccupancy = + layout.cells.filter((cell) => hexDistance(cell, { q: 0, r: 0 }) === radius) + .length / + (6 * radius); + assert.ok( + outerOccupancy >= 0.1 && outerOccupancy <= 0.55, + `${deploymentCount} deployments need a sparse, notched outer edge (${outerOccupancy})`, + ); +} + +function deployments(count) { + const buckets = [1, 2, 4, 7]; + return Array.from({ length: count }, (_, index) => ({ + id: `deployment-${String(index + 1).padStart(4, "0")}`, + cellCount: buckets[index % buckets.length], + })); +} + +test("layout is deterministic and independent of input order", () => { + const input = deployments(80); + const first = layoutCommunityComputeHexTerritories(input); + const second = layoutCommunityComputeHexTerritories([...input].reverse()); + assert.deepEqual(first, second); +}); + +test("growing layouts stay balanced with sparse outer frontiers", () => { + for (const count of [24, 250, 1_000]) { + const layout = layoutCommunityComputeHexTerritories(deployments(count)); + assertStructuralInvariants(layout); + assertBalancedSparseFrontier(layout, count); + assert.ok( + organicGaps(layout).length > 0, + `${count} deployments should leave deterministic pockets or edge gaps`, + ); + } +}); + +test("canonical territory order remains stable when previous placement is reused", () => { + const input = deployments(60); + const initial = layoutCommunityComputeHexTerritories(input); + const next = layoutCommunityComputeHexTerritories([...input].reverse(), { + previous: initial, + }); + assert.deepEqual( + next.territories.map(({ deploymentId }) => deploymentId), + initial.territories.map(({ deploymentId }) => deploymentId), + ); +}); + +test("payload changes preserve positions because IDs own the slots", () => { + const initial = deployments(40); + const changed = initial.map((deployment) => ({ + ...deployment, + label: `Changed ${deployment.id}`, + health: "degraded", + })); + assert.deepEqual( + layoutCommunityComputeHexTerritories(initial), + layoutCommunityComputeHexTerritories(changed), + ); +}); + +test("surviving IDs keep positions as deployments are added and removed", () => { + const initialInput = deployments(40); + const initial = layoutCommunityComputeHexTerritories(initialInput); + const nextInput = [ + ...initialInput.filter((_, index) => index % 4 !== 0), + ...deployments(6).map((item, index) => ({ + ...item, + id: `new-deployment-${index}`, + })), + ]; + const next = layoutCommunityComputeHexTerritories(nextInput, { + previous: initial, + }); + const initialCenters = new Map( + initial.territories.map((territory) => [ + territory.deploymentId, + territory.center, + ]), + ); + + for (const territory of next.territories) { + const oldCenter = initialCenters.get(territory.deploymentId); + if (oldCenter !== undefined) { + assert.deepEqual(territory.center, oldCenter); + } + } + assert.equal( + new Set(next.cells.map(hexCoordinateKey)).size, + next.cells.length, + ); +}); + +test("every supported territory shape is contiguous", () => { + const layout = layoutCommunityComputeHexTerritories([ + { id: "one", cellCount: 1 }, + { id: "two", cellCount: 2 }, + { id: "four", cellCount: 4 }, + { id: "seven", cellCount: 7 }, + ]); + const expected = new Map([ + ["one", 1], + ["two", 2], + ["four", 4], + ["seven", 7], + ]); + + for (const territory of layout.territories) { + assert.equal(territory.cells.length, expected.get(territory.deploymentId)); + assert.ok( + isContiguousHexTerritory(territory.cells), + `${territory.deploymentId} must be contiguous`, + ); + } +}); + +test("fused boundaries omit interior edges for 1, 2, 4, and 7 cells", () => { + const shapes = [ + [{ q: 0, r: 0 }], + [ + { q: 0, r: 0 }, + { q: 1, r: 0 }, + ], + [ + { q: 0, r: 0 }, + { q: 1, r: 0 }, + { q: 1, r: -1 }, + { q: 0, r: -1 }, + ], + [{ q: 0, r: 0 }, ...hexNeighbours({ q: 0, r: 0 })], + ]; + + for (const [index, cells] of shapes.entries()) { + const sharedPairs = + cells.reduce( + (total, cell) => + total + + hexNeighbours(cell).filter((neighbour) => + cells.some( + (candidate) => + hexCoordinateKey(candidate) === hexCoordinateKey(neighbour), + ), + ).length, + 0, + ) / 2; + const boundary = hexTerritoryBoundaryEdges(cells); + assert.equal(boundary.length, 6 * cells.length - 2 * sharedPairs); + + const endpointDegree = new Map(); + for (const edge of boundary) { + assert.notDeepEqual(edge.start, edge.end); + for (const point of [edge.start, edge.end]) { + const key = `${point.x.toFixed(8)},${point.y.toFixed(8)}`; + endpointDegree.set(key, (endpointDegree.get(key) ?? 0) + 1); + } + } + assert.ok( + [...endpointDegree.values()].every((degree) => degree === 2), + `shape ${index} boundary must form closed loops without dangling edges`, + ); + } + + const edgeKey = (edge) => + [edge.start, edge.end] + .map((point) => `${point.x.toFixed(8)},${point.y.toFixed(8)}`) + .sort() + .join("|"); + assert.deepEqual( + hexTerritoryBoundaryEdges(shapes[3]).map(edgeKey).sort(), + hexTerritoryBoundaryEdges([...shapes[3]].reverse()) + .map(edgeKey) + .sort(), + "edge membership must not depend on cell order", + ); +}); + +test("interior edges contain each shared device boundary exactly once", () => { + const cells = [{ q: 0, r: 0 }, ...hexNeighbours({ q: 0, r: 0 })]; + const interior = hexTerritoryInteriorEdges(cells); + const edgeKey = (edge) => + [edge.start, edge.end] + .map((point) => `${point.x.toFixed(8)},${point.y.toFixed(8)}`) + .sort() + .join("|"); + + assert.equal(interior.length, 12); + assert.equal(new Set(interior.map(edgeKey)).size, interior.length); + assert.deepEqual( + interior.map(edgeKey).sort(), + hexTerritoryInteriorEdges([...cells].reverse()) + .map(edgeKey) + .sort(), + "shared edge membership must not depend on cell order", + ); + assert.deepEqual(hexTerritoryInteriorEdges([{ q: 0, r: 0 }]), []); +}); + +test("territories never emit duplicate occupied cells", () => { + const layout = layoutCommunityComputeHexTerritories(deployments(250)); + const coordinates = layout.cells.map(hexCoordinateKey); + assert.equal(new Set(coordinates).size, coordinates.length); + assert.equal( + new Set(layout.cells.map((cell) => cell.id)).size, + layout.cells.length, + ); +}); + +test("duplicate deployment IDs are rejected rather than overlapped", () => { + assert.throws( + () => + layoutCommunityComputeHexTerritories([ + { id: "same", cellCount: 1 }, + { id: "same", cellCount: 7 }, + ]), + /Duplicate community compute deployment ID: same/, + ); +}); + +test("empty layout has no fabricated bounds", () => { + assert.deepEqual(layoutCommunityComputeHexTerritories([]), { + territories: [], + cells: [], + bounds: null, + }); +}); + +test("500-deployment fixture lays out uniquely and contiguously", () => { + const model = deriveCommunityComputeMapModel( + LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE.snapshot, + ); + const input = model.deployments.map(({ id, cellCount }) => ({ + id, + cellCount, + })); + const layout = layoutCommunityComputeHexTerritories(input); + + assert.equal(layout.territories.length, model.deployments.length); + assert.equal( + layout.cells.length, + model.deployments.reduce( + (total, deployment) => total + deployment.cellCount, + 0, + ), + ); + assert.equal( + new Set(layout.cells.map(hexCoordinateKey)).size, + layout.cells.length, + ); + assert.ok( + layout.territories.every((territory) => + isContiguousHexTerritory(territory.cells), + ), + ); + assert.ok(layout.bounds !== null); +}); diff --git a/desktop/src/features/mesh-compute/communityComputeHexLayout.ts b/desktop/src/features/mesh-compute/communityComputeHexLayout.ts new file mode 100644 index 00000000000..aac860d99ae --- /dev/null +++ b/desktop/src/features/mesh-compute/communityComputeHexLayout.ts @@ -0,0 +1,448 @@ +import type { HexCellCount } from "./communityComputeMapModel"; + +/** Axial coordinates for a pointy-top hex grid. */ +export type HexCoordinate = { + q: number; + r: number; +}; + +/** Cartesian point in pointy-top hex geometry. */ +export type HexPoint = { + x: number; + y: number; +}; + +/** One exposed side of a fused hex territory boundary. */ +export type HexBoundaryEdge = { + start: HexPoint; + end: HexPoint; +}; + +export type CommunityComputeLayoutInput = { + id: string; + cellCount: HexCellCount; +}; + +export type CommunityComputeHexCell = HexCoordinate & { + id: string; + deploymentId: string; + index: number; +}; + +export type CommunityComputeHexTerritory = { + deploymentId: string; + center: HexCoordinate; + cells: readonly CommunityComputeHexCell[]; +}; + +export type CommunityComputeHexBounds = { + minQ: number; + maxQ: number; + minR: number; + maxR: number; +}; + +export type CommunityComputeHexLayout = { + territories: readonly CommunityComputeHexTerritory[]; + cells: readonly CommunityComputeHexCell[]; + bounds: CommunityComputeHexBounds | null; +}; + +export type CommunityComputeHexLayoutOptions = { + /** + * Reuse centres for IDs that are still present. Supplying the last layout + * keeps a live map steady while deployments arrive, leave, or change size. + */ + previous?: CommunityComputeHexLayout | null; +}; + +const HEX_DIRECTIONS: readonly HexCoordinate[] = [ + { q: 1, r: 0 }, + { q: 1, r: -1 }, + { q: 0, r: -1 }, + { q: -1, r: 0 }, + { q: -1, r: 1 }, + { q: 0, r: 1 }, +]; + +/** + * Lay deployments out as one organic, edge-connected honeycomb. + * + * Input order never affects the result. Stable hashes choose territory + * orientation, while a deterministic low-discrepancy permutation spreads each + * ring's placements around the whole current frontier. Completed inner rings + * remain dense; only the growing outer layers carry deliberate notches. With a + * previous layout, collision-free surviving shapes retain their exact cells; + * new shapes take the first non-overlapping frontier position that shares an + * edge with the occupied mesh. + */ +export function layoutCommunityComputeHexTerritories( + deployments: readonly CommunityComputeLayoutInput[], + options: CommunityComputeHexLayoutOptions = {}, +): CommunityComputeHexLayout { + const seenIds = new Set(); + for (const deployment of deployments) { + if (seenIds.has(deployment.id)) { + throw new Error( + `Duplicate community compute deployment ID: ${deployment.id}`, + ); + } + seenIds.add(deployment.id); + } + + const ordered = [...deployments].sort( + (left, right) => + stableHash(left.id) - stableHash(right.id) || + left.id.localeCompare(right.id), + ); + // Previous cells are retained only when they remain collision-free. New + // territories search the compact spiral for the first candidate that both + // fits and touches the occupied frontier. + const previousById = new Map( + (options.previous?.territories ?? []).map((territory) => [ + territory.deploymentId, + territory, + ]), + ); + const retainableIds = connectedRetainedTerritoryIds(ordered, previousById); + const occupied = new Set(); + const territories: CommunityComputeHexTerritory[] = []; + const placementOrder = [...ordered].sort( + (left, right) => + Number(retainableIds.has(right.id)) - + Number(retainableIds.has(left.id)) || + ordered.indexOf(left) - ordered.indexOf(right), + ); + + for (const deployment of placementOrder) { + const previous = retainableIds.has(deployment.id) + ? previousById.get(deployment.id) + : undefined; + const retained = + previous?.cells.length === deployment.cellCount + ? previous.cells.map(({ q, r }) => ({ q, r })) + : null; + const retainedFits = retained?.every( + (cell) => !occupied.has(hexCoordinateKey(cell)), + ); + const orientation = stableHash(`${deployment.id}:orientation`) % 6; + let center = retainedFits ? previous?.center : undefined; + let coordinates = retainedFits ? retained : null; + + if (!coordinates || !center) { + for (let slot = 0; ; slot += 1) { + const candidate = balancedSpiralCoordinate(slot); + const offsets = territoryOffsets(deployment.cellCount, orientation); + const candidateCells = offsets.map((offset) => ({ + q: candidate.q + offset.q, + r: candidate.r + offset.r, + })); + const fits = candidateCells.every( + (cell) => !occupied.has(hexCoordinateKey(cell)), + ); + const touches = + occupied.size === 0 || + candidateCells.some((cell) => + hexNeighbours(cell).some((neighbour) => + occupied.has(hexCoordinateKey(neighbour)), + ), + ); + if (fits && touches) { + center = candidate; + coordinates = candidateCells; + break; + } + } + } + + const cells = coordinates.map(({ q, r }, index) => { + occupied.add(hexCoordinateKey({ q, r })); + return { + id: `${deployment.id}@${q},${r}`, + deploymentId: deployment.id, + index, + q, + r, + }; + }); + territories.push({ deploymentId: deployment.id, center, cells }); + } + territories.sort( + (left, right) => + ordered.findIndex((item) => item.id === left.deploymentId) - + ordered.findIndex((item) => item.id === right.deploymentId), + ); + const cells = territories.flatMap((territory) => territory.cells); + + return { territories, cells, bounds: findBounds(cells) }; +} + +/** The six axial neighbours of a cell, in stable clockwise order. */ +export function hexNeighbours(coordinate: HexCoordinate): HexCoordinate[] { + return HEX_DIRECTIONS.map((direction) => ({ + q: coordinate.q + direction.q, + r: coordinate.r + direction.r, + })); +} + +/** + * Return only the exposed unit-hex sides of a territory. + * + * Coordinates use pointy-top axial geometry. Shared sides are omitted, so the + * result can be stroked as one fused territory rather than as overlapping + * individual hexagons. Edge direction is stable and follows each cell's + * perimeter; callers may chain matching endpoints when an ordered path is + * required. + */ +export function hexTerritoryBoundaryEdges( + cells: readonly HexCoordinate[], +): HexBoundaryEdge[] { + const occupied = new Set(cells.map(hexCoordinateKey)); + const edges: HexBoundaryEdge[] = []; + + for (const cell of cells) { + for (let direction = 0; direction < HEX_DIRECTIONS.length; direction += 1) { + const neighbour = { + q: cell.q + HEX_DIRECTIONS[direction].q, + r: cell.r + HEX_DIRECTIONS[direction].r, + }; + if (!occupied.has(hexCoordinateKey(neighbour))) { + edges.push(hexEdge(cell, direction)); + } + } + } + + return edges; +} + +/** + * Return each side shared by two cells in a territory exactly once. + * + * Fused model territories do not render these edges. Split deployments use + * them to show the device boundaries without drawing inset outlines around + * every cell or accidentally doubling a shared stroke. + */ +export function hexTerritoryInteriorEdges( + cells: readonly HexCoordinate[], +): HexBoundaryEdge[] { + const occupied = new Set(cells.map(hexCoordinateKey)); + const edges: HexBoundaryEdge[] = []; + + for (const cell of cells) { + const cellKey = hexCoordinateKey(cell); + for (let direction = 0; direction < HEX_DIRECTIONS.length; direction += 1) { + const neighbour = { + q: cell.q + HEX_DIRECTIONS[direction].q, + r: cell.r + HEX_DIRECTIONS[direction].r, + }; + const neighbourKey = hexCoordinateKey(neighbour); + if (occupied.has(neighbourKey) && cellKey < neighbourKey) { + edges.push(hexEdge(cell, direction)); + } + } + } + + return edges; +} + +function hexEdge(cell: HexCoordinate, direction: number): HexBoundaryEdge { + const center = { + x: Math.sqrt(3) * (cell.q + cell.r / 2), + y: 1.5 * cell.r, + }; + const corners = Array.from({ length: 6 }, (_, index) => { + const angle = ((-30 + index * 60) * Math.PI) / 180; + return { + x: normalizeHexPointComponent(center.x + Math.cos(angle)), + y: normalizeHexPointComponent(center.y + Math.sin(angle)), + }; + }); + return { + start: corners[(6 - direction) % 6], + end: corners[(7 - direction) % 6], + }; +} + +function normalizeHexPointComponent(value: number): number { + return Math.abs(value) < Number.EPSILON * 8 ? 0 : value; +} + +/** Axial hex distance, useful to consumers for borders and transitions. */ +export function hexDistance(left: HexCoordinate, right: HexCoordinate): number { + const dq = left.q - right.q; + const dr = left.r - right.r; + return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(dq + dr)); +} + +/** True when every cell can be reached from every other through shared edges. */ +export function isContiguousHexTerritory( + cells: readonly HexCoordinate[], +): boolean { + if (cells.length === 0) { + return false; + } + const remaining = new Set(cells.map(hexCoordinateKey)); + const queue: HexCoordinate[] = [cells[0]]; + remaining.delete(hexCoordinateKey(cells[0])); + for (let cursor = 0; cursor < queue.length; cursor += 1) { + for (const neighbour of hexNeighbours(queue[cursor])) { + const key = hexCoordinateKey(neighbour); + if (remaining.delete(key)) { + queue.push(neighbour); + } + } + } + return remaining.size === 0; +} + +export function hexCoordinateKey(coordinate: HexCoordinate): string { + return `${coordinate.q},${coordinate.r}`; +} + +/** Keep only the edge-connected component of valid surviving territories. */ +function connectedRetainedTerritoryIds( + ordered: readonly CommunityComputeLayoutInput[], + previousById: ReadonlyMap, +): Set { + const valid = ordered + .map((deployment) => previousById.get(deployment.id)) + .filter( + (territory): territory is CommunityComputeHexTerritory => + territory !== undefined && + territory.cells.length === + ordered.find((item) => item.id === territory.deploymentId)?.cellCount, + ); + if (valid.length === 0) return new Set(); + + const owner = new Map(); + for (const territory of valid) { + for (const cell of territory.cells) { + const key = hexCoordinateKey(cell); + if (owner.has(key)) return new Set(); + owner.set(key, territory.deploymentId); + } + } + const retained = new Set([valid[0].deploymentId]); + const queue = [valid[0].deploymentId]; + const byId = new Map( + valid.map((territory) => [territory.deploymentId, territory]), + ); + for (let cursor = 0; cursor < queue.length; cursor += 1) { + const territory = byId.get(queue[cursor]); + if (!territory) continue; + for (const cell of territory.cells) { + for (const neighbour of hexNeighbours(cell)) { + const neighbourOwner = owner.get(hexCoordinateKey(neighbour)); + if (neighbourOwner && !retained.has(neighbourOwner)) { + retained.add(neighbourOwner); + queue.push(neighbourOwner); + } + } + } + } + return retained; +} + +function territoryOffsets( + count: HexCellCount, + orientation: number, +): HexCoordinate[] { + const center = { q: 0, r: 0 }; + if (count === 1) { + return [center]; + } + if (count === 2) { + return [center, HEX_DIRECTIONS[orientation]]; + } + if (count === 4) { + return [ + center, + HEX_DIRECTIONS[orientation], + HEX_DIRECTIONS[(orientation + 1) % 6], + HEX_DIRECTIONS[(orientation + 2) % 6], + ]; + } + return [center, ...HEX_DIRECTIONS]; +} + +/** + * Zero-based spiral slot: origin, then complete radius-one, radius-two, … rings. + * Runtime is O(radius), which is O(sqrt(n)); even at dense fixture sizes the + * largest radius remains small. + */ +function balancedSpiralCoordinate(index: number): HexCoordinate { + if (index === 0) { + return { q: 0, r: 0 }; + } + + let radius = 1; + let ringStart = 1; + while (index >= ringStart + 6 * radius) { + ringStart += 6 * radius; + radius += 1; + } + + const ringLength = 6 * radius; + const ringIndex = index - ringStart; + // Multiplication by a number coprime with the ring length is a permutation, + // not sampling: every slot is eventually visited. A step near half a ring + // alternates opposite sides, preventing a partially filled layer from + // producing the one-sided growth of a clockwise spiral. + let step = Math.max(1, Math.floor(ringLength / 2) - 1); + while (greatestCommonDivisor(step, ringLength) !== 1) step -= 1; + const offsetSeed = stableHash(`ring:${radius}`) % ringLength; + let offset = (offsetSeed + ringIndex * step) % ringLength; + let coordinate = { q: -radius, r: radius }; + for (const direction of HEX_DIRECTIONS) { + const steps = Math.min(radius, offset); + coordinate = { + q: coordinate.q + direction.q * steps, + r: coordinate.r + direction.r * steps, + }; + offset -= steps; + if (offset === 0) { + break; + } + } + return coordinate; +} + +function greatestCommonDivisor(left: number, right: number): number { + let a = left; + let b = right; + while (b !== 0) { + const remainder = a % b; + a = b; + b = remainder; + } + return a; +} + +function findBounds( + cells: readonly HexCoordinate[], +): CommunityComputeHexBounds | null { + if (cells.length === 0) { + return null; + } + let minQ = cells[0].q; + let maxQ = cells[0].q; + let minR = cells[0].r; + let maxR = cells[0].r; + for (const cell of cells.slice(1)) { + minQ = Math.min(minQ, cell.q); + maxQ = Math.max(maxQ, cell.q); + minR = Math.min(minR, cell.r); + maxR = Math.max(maxR, cell.r); + } + return { minQ, maxQ, minR, maxR }; +} + +/** FNV-1a over UTF-16 code units; deterministic on every JS runtime. */ +function stableHash(value: string): number { + let hash = 0x811c9dc5; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 0x01000193); + } + return hash >>> 0; +} diff --git a/desktop/src/features/mesh-compute/communityComputeMapModel.test.mjs b/desktop/src/features/mesh-compute/communityComputeMapModel.test.mjs new file mode 100644 index 00000000000..e9c3ed32b24 --- /dev/null +++ b/desktop/src/features/mesh-compute/communityComputeMapModel.test.mjs @@ -0,0 +1,206 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + bucketHexCellCount, + capacityGbToHexCellCount, + deriveCommunityComputeMapModel, +} from "./communityComputeMapModel.ts"; +import { + COMMUNITY_COMPUTE_TUTORIAL_FIXTURES, + LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE, + createLargeDenseSnapshot, +} from "./communityComputeFixtures.ts"; + +function device(overrides = {}) { + return { + deviceId: "device-1", + label: "Studio", + capacityGb: 32, + models: ["gemma"], + state: "serving", + isSelf: false, + memberPubkey: "member-1", + reportedAt: 1_786_032_000, + modelSizeGb: 18, + ...overrides, + }; +} + +function snapshot(overrides = {}) { + return { + sharingDeviceCount: 1, + contributorMemberCount: 1, + sharedCapacityGb: 32, + allocatedCapacityGb: 18, + models: ["gemma"], + devices: [device()], + includesSelf: false, + memberCount: 4, + reason: null, + ...overrides, + }; +} + +test("cell buckets are exactly 1, 2, 4, and 7", () => { + assert.deepEqual( + [-1, 1, 1.1, 2, 2.1, 4, 4.1, 999, Number.NaN].map(bucketHexCellCount), + [1, 1, 2, 2, 4, 4, 7, 7, 1], + ); + assert.deepEqual( + [null, 128, 128.1, 256, 256.1, 512, 512.1, 1000].map( + capacityGbToHexCellCount, + ), + [1, 1, 2, 2, 4, 4, 7, 7], + ); +}); + +test("only ready serving models become deployments", () => { + const model = deriveCommunityComputeMapModel( + snapshot({ + sharingDeviceCount: 1, + devices: [ + device({ deviceId: "a", models: ["gemma", "qwen"] }), + device({ deviceId: "b", state: "loading", models: [] }), + device({ deviceId: "c", state: "standby", models: [] }), + device({ deviceId: "d", state: "consuming", models: [] }), + ], + }), + ); + + assert.equal(model.status, "ready"); + assert.deepEqual( + model.deployments.map((deployment) => deployment.modelId), + ["gemma", "qwen"], + ); + assert.equal(model.kpis.deploymentCount, 2); + assert.equal(model.kpis.contributorMemberCount, 1); + assert.equal(model.health.counts.healthy, 1); + assert.equal(model.health.counts.warming, 1); + assert.equal(model.health.counts.idle, 1); +}); + +test("one territory is produced per model and per device replica", () => { + const model = deriveCommunityComputeMapModel( + snapshot({ + devices: [ + device({ deviceId: "a", models: ["qwen", "gemma"] }), + device({ deviceId: "b", memberPubkey: "member-2", models: ["gemma"] }), + ], + }), + ); + assert.equal(model.deployments.length, 3); + assert.equal( + model.deployments.filter((deployment) => deployment.modelId === "gemma") + .length, + 2, + ); + assert.equal(new Set(model.deployments.map(({ id }) => id)).size, 3); +}); + +test("reported model footprint controls area without fixture overrides", () => { + const model = deriveCommunityComputeMapModel( + snapshot({ devices: [device({ capacityGb: 768, modelSizeGb: 96 })] }), + ); + assert.equal(model.deployments[0].cellCount, 1); + + const large = deriveCommunityComputeMapModel( + snapshot({ devices: [device({ capacityGb: 768, modelSizeGb: 646 })] }), + ); + assert.equal(large.deployments[0].cellCount, 7); +}); + +test("loading, consumer-only, and empty snapshots render no territories", () => { + for (const state of ["loading", "standby", "consuming"]) { + const model = deriveCommunityComputeMapModel( + snapshot({ devices: [device({ state, models: [] })] }), + ); + assert.equal(model.status, "empty"); + assert.equal(model.deployments.length, 0); + } + assert.equal(deriveCommunityComputeMapModel(null).status, "loading"); + assert.equal( + deriveCommunityComputeMapModel( + COMMUNITY_COMPUTE_TUTORIAL_FIXTURES.empty.snapshot, + ).status, + "empty", + ); +}); + +test("projection is deterministic and independent of device/model order", () => { + const devices = [ + device({ deviceId: "z", label: "Zulu", models: ["qwen", "gemma"] }), + device({ deviceId: "a", label: "Alpha", models: ["mistral"] }), + ]; + const forward = deriveCommunityComputeMapModel(snapshot({ devices })); + const reverse = deriveCommunityComputeMapModel( + snapshot({ + devices: [...devices] + .reverse() + .map((item) => ({ ...item, models: [...item.models].reverse() })), + }), + ); + assert.deepEqual(forward, reverse); +}); + +test("tutorial fixtures use only production fields and production sizing", () => { + assert.deepEqual(Object.keys(COMMUNITY_COMPUTE_TUTORIAL_FIXTURES).sort(), [ + "empty", + "large-dense-500", + "single-device", + "small-company", + ]); + const allowed = new Set([ + "deviceId", + "label", + "capacityGb", + "models", + "state", + "isSelf", + "memberPubkey", + "reportedAt", + "modelSizeGb", + ]); + for (const fixture of Object.values(COMMUNITY_COMPUTE_TUTORIAL_FIXTURES)) { + for (const item of fixture.snapshot.devices) { + assert.deepEqual( + Object.keys(item).filter((key) => !allowed.has(key)), + [], + ); + } + } + const small = deriveCommunityComputeMapModel( + COMMUNITY_COMPUTE_TUTORIAL_FIXTURES["small-company"].snapshot, + ); + assert.equal(small.deployments.length, 12); + assert.equal( + small.deployments.filter(({ deviceLabel }) => + deviceLabel.startsWith("Team MacBook"), + ).length, + 10, + ); + assert.equal( + small.deployments.filter(({ cellCount }) => cellCount === 1).length, + 10, + ); + assert.equal( + small.deployments.filter(({ cellCount }) => cellCount === 7).length, + 2, + ); +}); + +test("large dense fixture is deterministic and excludes non-serving rows", () => { + const fixture = LARGE_DENSE_COMMUNITY_COMPUTE_FIXTURE.snapshot; + assert.deepEqual(fixture, createLargeDenseSnapshot(500)); + assert.equal(fixture.devices.length, 500); + const servingCount = fixture.devices.filter( + (item) => item.state === "serving" && item.models.length > 0, + ).length; + const model = deriveCommunityComputeMapModel(fixture); + assert.equal(model.deployments.length, servingCount); + assert.equal(model.kpis.deploymentCount, servingCount); + assert.equal( + new Set(model.deployments.map(({ id }) => id)).size, + servingCount, + ); +}); diff --git a/desktop/src/features/mesh-compute/communityComputeMapModel.ts b/desktop/src/features/mesh-compute/communityComputeMapModel.ts new file mode 100644 index 00000000000..663b957d5f9 --- /dev/null +++ b/desktop/src/features/mesh-compute/communityComputeMapModel.ts @@ -0,0 +1,365 @@ +import type { + MeshDeviceState, + MeshSnapshot, + MeshSnapshotDevice, +} from "@/shared/api/tauriMesh"; + +/** The only territory sizes rendered by the community compute map. */ +export const HEX_CELL_BUCKETS = [1, 2, 4, 7] as const; + +export type HexCellCount = (typeof HEX_CELL_BUCKETS)[number]; + +/** + * The Community view intentionally consumes only fields in the production + * snapshot contract. Tutorial fixtures use this same shape so examples cannot + * quietly grow capabilities that live Compute does not have. + */ +export type CommunityComputeDeviceInput = MeshSnapshotDevice; + +/** A readonly, structurally compatible form of the current mesh snapshot. */ +export type CommunityComputeSnapshotInput< + TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput, +> = Omit & { + readonly devices: readonly TDevice[]; +}; + +export type CommunityComputeDeploymentHealth = + | "healthy" + | "warming" + | "idle" + | "degraded" + | "offline" + | "unknown"; + +export type CommunityComputeMapHealth = + | "healthy" + | "attention" + | "critical" + | "empty" + | "unknown"; + +export type CommunityComputeDeployment< + TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput, +> = { + /** Stable map identity. Never derived from the device's array position. */ + id: string; + /** Model shown as the territory's primary identity. */ + label: string; + modelId: string; + deviceLabel: string; + capacityGb: number | null; + models: readonly string[]; + state: MeshDeviceState; + isSelf: boolean; + health: CommunityComputeDeploymentHealth; + healthReason: string | null; + cellCount: HexCellCount; + /** Preserves additive backend data for a future detail projection. */ + source: Readonly; +}; + +export type CommunityComputeKpis = { + memberCount: number; + contributorMemberCount: number; + deploymentCount: number; + activeDeploymentCount: number; + sharedCapacityGb: number | null; + allocatedCapacityGb: number | null; + allocatedPercent: number | null; + modelCount: number; +}; + +export type CommunityComputeHealthSummary = { + status: CommunityComputeMapHealth; + label: string; + reason: string | null; + counts: Record; +}; + +export type CommunityComputeMapModel< + TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput, +> = { + status: "loading" | "ready" | "empty"; + kpis: CommunityComputeKpis; + health: CommunityComputeHealthSummary; + deployments: readonly CommunityComputeDeployment[]; +}; + +/** + * Round a desired territory footprint up to one of the four supported shapes. + * Invalid and non-positive weights still get one cell so a deployment cannot + * disappear from the map. + */ +export function bucketHexCellCount( + weight: number | null | undefined, +): HexCellCount { + if (weight === null || weight === undefined || !Number.isFinite(weight)) { + return 1; + } + const normalized = Math.max(1, Math.ceil(weight)); + return HEX_CELL_BUCKETS.find((bucket) => normalized <= bucket) ?? 7; +} + +/** + * Turn model memory into a deliberately coarse footprint. + * + * A 128 GB Mac is the common mesh workhorse, so every model that fits on one is + * one cell. Multi-cell territories are reserved for genuinely multi-GPU scale + * deployments; this avoids making ordinary local models look fragmented. + */ +export function capacityGbToHexCellCount( + capacityGb: number | null | undefined, +): HexCellCount { + if (capacityGb === null || capacityGb === undefined || capacityGb <= 128) { + return 1; + } + if (capacityGb <= 256) { + return 2; + } + if (capacityGb <= 512) { + return 4; + } + return 7; +} + +const EMPTY_HEALTH_COUNTS: Record = { + healthy: 0, + warming: 0, + idle: 0, + degraded: 0, + offline: 0, + unknown: 0, +}; + +/** Project a backend snapshot into the complete, render-agnostic map model. */ +export function deriveCommunityComputeMapModel< + TDevice extends MeshSnapshotDevice = CommunityComputeDeviceInput, +>( + snapshot: CommunityComputeSnapshotInput | null, +): CommunityComputeMapModel { + if (snapshot === null) { + return { + status: "loading", + kpis: { + memberCount: 0, + contributorMemberCount: 0, + deploymentCount: 0, + activeDeploymentCount: 0, + sharedCapacityGb: null, + allocatedCapacityGb: null, + allocatedPercent: null, + modelCount: 0, + }, + health: { + status: "unknown", + label: "Checking community compute", + reason: null, + counts: { ...EMPTY_HEALTH_COUNTS }, + }, + deployments: [], + }; + } + + const deployments = projectDeployments(snapshot.devices); + const healthStates = snapshot.devices + .filter((device) => device.state !== "consuming") + .map((device) => healthFromState(device.state)); + const counts = countHealth(healthStates); + const models = new Set(snapshot.models); + for (const deployment of deployments) { + for (const model of deployment.models) { + models.add(model); + } + } + + const contributorMemberCount = + snapshot.contributorMemberCount ?? + new Set( + deployments + .filter((deployment) => deployment.state === "serving") + .map( + (deployment) => + deployment.source.memberPubkey?.trim() || deployment.id, + ), + ).size; + const sharedCapacityGb = finiteNonNegative(snapshot.sharedCapacityGb); + const reportedAllocated = finiteNonNegative(snapshot.allocatedCapacityGb); + const servingDeployments = deployments.filter( + (deployment) => deployment.state === "serving", + ); + const inferredAllocations = servingDeployments.map((deployment) => + finiteNonNegative(deployment.source.modelSizeGb), + ); + const allocatedCapacityGb = + reportedAllocated ?? + (inferredAllocations.length > 0 && + inferredAllocations.every((value) => value !== null) + ? inferredAllocations.reduce((total, value) => total + (value ?? 0), 0) + : null); + + return { + status: deployments.length === 0 ? "empty" : "ready", + kpis: { + memberCount: nonNegativeInteger(snapshot.memberCount), + contributorMemberCount, + deploymentCount: deployments.length, + activeDeploymentCount: counts.healthy + counts.warming, + sharedCapacityGb, + allocatedCapacityGb, + allocatedPercent: + allocatedCapacityGb !== null && + sharedCapacityGb !== null && + sharedCapacityGb > 0 + ? Math.min(100, (allocatedCapacityGb / sharedCapacityGb) * 100) + : null, + modelCount: models.size, + }, + health: summarizeHealth(counts, healthStates, snapshot.reason), + deployments, + }; +} + +function projectDeployments( + devices: readonly TDevice[], +): CommunityComputeDeployment[] { + const canonical = devices + .filter((device) => device.state === "serving" && device.models.length > 0) + .flatMap((device) => + [...new Set(device.models)].map((model) => ({ + device: { ...device, models: [model] } as TDevice, + identity: deploymentIdentity(device, model), + })), + ) + .sort( + (left, right) => + left.identity.localeCompare(right.identity) || + deviceFingerprint(left.device).localeCompare( + deviceFingerprint(right.device), + ), + ); + const occurrences = new Map(); + + return canonical.map(({ device, identity }) => { + const occurrence = occurrences.get(identity) ?? 0; + occurrences.set(identity, occurrence + 1); + const models = [...new Set(device.models)].sort(); + return { + id: occurrence === 0 ? identity : `${identity}#${occurrence + 1}`, + label: models[0] ?? device.label, + modelId: models[0] ?? "Model not reported", + deviceLabel: device.label, + capacityGb: finiteNonNegative(device.capacityGb), + models, + state: device.state, + isSelf: device.isSelf, + health: healthFromState(device.state), + healthReason: null, + cellCount: capacityGbToHexCellCount( + device.modelSizeGb ?? device.capacityGb, + ), + source: device, + }; + }); +} + +function deploymentIdentity( + device: MeshSnapshotDevice, + modelId: string, +): string { + const deviceId = cleanOptionalText(device.deviceId); + const model = encodeURIComponent(modelId); + return deviceId === null + ? `derived:${encodeURIComponent(deviceFingerprint(device))}:${model}` + : `deployment:${encodeURIComponent(deviceId)}:${model}`; +} + +function deviceFingerprint(device: MeshSnapshotDevice): string { + return JSON.stringify([ + device.label, + device.capacityGb, + [...device.models].sort(), + device.state, + device.isSelf, + ]); +} + +function healthFromState( + state: MeshDeviceState, +): CommunityComputeDeploymentHealth { + switch (state) { + case "serving": + case "consuming": + return "healthy"; + case "loading": + return "warming"; + case "standby": + return "idle"; + } +} + +function countHealth( + states: readonly CommunityComputeDeploymentHealth[], +): Record { + const counts = { ...EMPTY_HEALTH_COUNTS }; + for (const state of states) { + counts[state] += 1; + } + return counts; +} + +function summarizeHealth( + counts: Record, + states: readonly CommunityComputeDeploymentHealth[], + snapshotReason: string | null, +): CommunityComputeHealthSummary { + if (states.length === 0) { + return { + status: "empty", + label: "No deployments", + reason: cleanOptionalText(snapshotReason), + counts, + }; + } + + const unavailable = counts.offline + counts.degraded; + const reason = null; + + if (counts.offline === states.length) { + return { status: "critical", label: "Compute unavailable", reason, counts }; + } + if (unavailable > 0) { + return { + status: "attention", + label: `${unavailable} deployment${unavailable === 1 ? "" : "s"} need attention`, + reason, + counts, + }; + } + if (counts.unknown === states.length) { + return { status: "unknown", label: "Health unknown", reason: null, counts }; + } + return { + status: "healthy", + label: "No reported issues", + reason: null, + counts, + }; +} + +function finiteNonNegative(value: number | null | undefined): number | null { + return value !== null && + value !== undefined && + Number.isFinite(value) && + value >= 0 + ? value + : null; +} + +function nonNegativeInteger(value: number): number { + return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; +} + +function cleanOptionalText(value: string | null | undefined): string | null { + const cleaned = value?.trim(); + return cleaned ? cleaned : null; +} diff --git a/desktop/src/features/mesh-compute/hooks/useMeshNodeStatus.ts b/desktop/src/features/mesh-compute/hooks/useMeshNodeStatus.ts index c9194a0e055..2bc632d84bb 100644 --- a/desktop/src/features/mesh-compute/hooks/useMeshNodeStatus.ts +++ b/desktop/src/features/mesh-compute/hooks/useMeshNodeStatus.ts @@ -17,42 +17,54 @@ export function useMeshNodeStatus(): { status: MeshNodeStatus | null; error: string | null; refresh: () => void; + update: (status: MeshNodeStatus) => void; } { const [status, setStatus] = React.useState(null); const [error, setError] = React.useState(null); + const requestSequence = React.useRef(0); const fetchOnce = React.useCallback(() => { - let cancelled = false; - (async () => { + const sequence = ++requestSequence.current; + void (async () => { try { const value = await meshNodeStatus(); - if (!cancelled) { + if (sequence === requestSequence.current) { setStatus(value); setError(null); } } catch (err) { - if (!cancelled) { + if (sequence === requestSequence.current) { setError(err instanceof Error ? err.message : String(err)); } } })(); - return () => { - cancelled = true; - }; }, []); - React.useEffect(() => fetchOnce(), [fetchOnce]); + const update = React.useCallback((value: MeshNodeStatus) => { + // Invalidate any slower request started before this authoritative command + // result so it cannot restore the preceding lifecycle state. + requestSequence.current += 1; + setStatus(value); + setError(null); + }, []); + + React.useEffect(() => { + fetchOnce(); + }, [fetchOnce]); // Fast poll while in a transitioning state; slow poll while steady or off. React.useEffect(() => { const transitioning = status?.state === "starting" || status?.state === "stopping"; - const interval = transitioning ? 750 : 4000; + // Keep reconciling aggressively while a start command is still waiting on + // model readiness. The command can take minutes and its eventual response + // is not the only signal that the runtime crossed to running. + const interval = transitioning ? 500 : 4000; const handle = window.setInterval(() => { fetchOnce(); }, interval); return () => window.clearInterval(handle); }, [status?.state, fetchOnce]); - return { status, error, refresh: fetchOnce }; + return { status, error, refresh: fetchOnce, update }; } diff --git a/desktop/src/features/mesh-compute/hooks/useMeshSnapshot.ts b/desktop/src/features/mesh-compute/hooks/useMeshSnapshot.ts new file mode 100644 index 00000000000..85de3d794d2 --- /dev/null +++ b/desktop/src/features/mesh-compute/hooks/useMeshSnapshot.ts @@ -0,0 +1,62 @@ +import * as React from "react"; + +import { meshSnapshot } from "@/shared/api/tauriMesh"; +import type { MeshSnapshot } from "@/shared/api/tauriMesh"; + +/** + * Poll the community shared-compute snapshot. + * + * Deliberately slow compared to `useMeshNodeStatus` (750ms/4000ms): that hook + * watches a LOCAL lifecycle that must feel responsive, while this one reads + * RELAY state that only changes as fast as the 45s member heartbeat, with a + * 120s freshness window. Polling faster would just add relay queries without + * adding information. + * + * An empty mesh is not an error — the snapshot carries its own `reason`. `error` + * here means the query itself failed (offline relay, stub build). + */ +const SNAPSHOT_POLL_INTERVAL_MS = 30_000; + +export function useMeshSnapshot(options?: { refreshKey?: number }): { + snapshot: MeshSnapshot | null; + error: string | null; + refresh: () => void; +} { + const [snapshot, setSnapshot] = React.useState(null); + const [error, setError] = React.useState(null); + const refreshKey = options?.refreshKey ?? 0; + + // Guards against a slow in-flight response overwriting a newer one. + const requestSeq = React.useRef(0); + + const fetchOnce = React.useCallback(() => { + const seq = ++requestSeq.current; + (async () => { + try { + const value = await meshSnapshot(); + if (seq === requestSeq.current) { + setSnapshot(value); + setError(null); + } + } catch (err) { + if (seq === requestSeq.current) { + setError(err instanceof Error ? err.message : String(err)); + } + } + })(); + }, []); + + // Re-fetch immediately when the local node transitions (refreshKey), so + // turning sharing on updates the community view without waiting for a poll. + // biome-ignore lint/correctness/useExhaustiveDependencies: refreshKey is an intentional trigger, not a value read in the body — bumping it re-queries the community snapshot right after a local node transition instead of waiting out the 30s poll + React.useEffect(() => { + fetchOnce(); + }, [fetchOnce, refreshKey]); + + React.useEffect(() => { + const handle = window.setInterval(fetchOnce, SNAPSHOT_POLL_INTERVAL_MS); + return () => window.clearInterval(handle); + }, [fetchOnce]); + + return { snapshot, error, refresh: fetchOnce }; +} diff --git a/desktop/src/features/mesh-compute/ui/CommunityComputeTerritoryMap.tsx b/desktop/src/features/mesh-compute/ui/CommunityComputeTerritoryMap.tsx new file mode 100644 index 00000000000..91dd44d2abb --- /dev/null +++ b/desktop/src/features/mesh-compute/ui/CommunityComputeTerritoryMap.tsx @@ -0,0 +1,342 @@ +import * as React from "react"; + +import { cn } from "@/shared/lib/cn"; +import { + hexTerritoryBoundaryEdges, + layoutCommunityComputeHexTerritories, + type CommunityComputeHexCell, +} from "../communityComputeHexLayout"; +import type { + CommunityComputeDeployment, + CommunityComputeMapModel, +} from "../communityComputeMapModel"; + +export type CommunityComputeTerritoryMapProps = { + model: CommunityComputeMapModel; + className?: string; +}; + +type Point = { x: number; y: number }; + +type ViewBox = { + x: number; + y: number; + width: number; + height: number; +}; + +// Deliberately overlap polygons, then trim their outside with the territory +// edge. This avoids SVG antialias seams inside one model footprint. +const FUSED_TILE_RADIUS = 1.025; +const TILE_EDGE_WIDTH = 0.1; +const SQRT_THREE = Math.sqrt(3); + +/** + * Responsive SVG territory map. Each deployment owns one stable, contiguous + * cluster of discrete hexes; health changes never move that cluster. + */ +export function CommunityComputeTerritoryMap({ + model, + className, +}: CommunityComputeTerritoryMapProps) { + const deploymentById = React.useMemo( + () => + new Map( + model.deployments.map((deployment) => [deployment.id, deployment]), + ), + [model.deployments], + ); + const previousLayoutRef = React.useRef | null>(null); + const layout = React.useMemo( + () => + layoutCommunityComputeHexTerritories( + model.deployments.map(({ id, cellCount }) => ({ id, cellCount })), + { previous: previousLayoutRef.current }, + ), + [model.deployments], + ); + React.useEffect(() => { + previousLayoutRef.current = layout; + }, [layout]); + const viewBox = computeViewBox(layout.cells); + const [hoveredId, setHoveredId] = React.useState(null); + const lowerTimerRef = React.useRef | null>( + null, + ); + const hoveredDeployment = hoveredId + ? (deploymentById.get(hoveredId) ?? null) + : null; + const isDenseMap = model.deployments.length >= 50; + + React.useEffect( + () => () => { + if (lowerTimerRef.current) clearTimeout(lowerTimerRef.current); + }, + [], + ); + + function raiseTerritory(deploymentId: string, territoryElement: SVGGElement) { + if (lowerTimerRef.current) clearTimeout(lowerTimerRef.current); + // SVG paints in DOM order. Move the existing node instead of rebuilding + // children so hover stays smooth as territories scale. + territoryElement.parentNode?.appendChild(territoryElement); + setHoveredId(deploymentId); + } + + function lowerTerritory() { + if (lowerTimerRef.current) clearTimeout(lowerTimerRef.current); + // Let the detail card linger briefly while the territory settles. + lowerTimerRef.current = setTimeout(() => setHoveredId(null), 280); + } + + if (model.status === "loading") { + return ( +
+
+

+ Mapping community compute… +

+
+ ); + } + + if (model.status === "empty" || viewBox === null) { + return ( +
+ +

No compute territories yet

+

+ Shared compute appears here when members make it available to the + community. +

+
+ ); + } + + return ( +
+
+ + {layout.territories.map((territory) => { + const deployment = deploymentById.get(territory.deploymentId); + if (!deployment) return null; + return ( + // SVG groups have no semantic element for hover/focus detail disclosure. + // biome-ignore lint/a11y/noStaticElementInteractions: keyboard focus mirrors hover without making the territory an action. + + raiseTerritory(deployment.id, event.currentTarget) + } + onMouseEnter={(event) => + raiseTerritory(deployment.id, event.currentTarget) + } + onMouseLeave={lowerTerritory} + style={territoryHoverStyle( + territory.cells, + viewBox, + isDenseMap, + )} + tabIndex={0} + > + {territoryAccessibleLabel(deployment)} + + + ); + })} + + {hoveredDeployment ? ( +
+

+ {shortModelName(hoveredDeployment.modelId)} +

+

+ {hoveredDeployment.deviceLabel} ·{" "} + {formatCapacity(hoveredDeployment.capacityGb)} +

+

+ Available to the community +

+
+ ) : null} +
+
+ ); +} + +function TerritoryShape({ + cells, +}: { + cells: readonly CommunityComputeHexCell[]; +}) { + const boundary = hexTerritoryBoundaryEdges(cells); + return ( + + {cells.map((cell) => { + const center = axialToPoint(cell.q, cell.r); + return ( + + ); + })} + + + ); +} + +function territoryHoverStyle( + cells: readonly CommunityComputeHexCell[], + viewBox: ViewBox, + dense: boolean, +): React.CSSProperties { + const centers = cells.map((cell) => axialToPoint(cell.q, cell.r)); + const minX = Math.min(...centers.map(({ x }) => x)) - 1; + const maxX = Math.max(...centers.map(({ x }) => x)) + 1; + const minY = Math.min(...centers.map(({ y }) => y)) - 1; + const maxY = Math.max(...centers.map(({ y }) => y)) + 1; + const territorySpan = Math.max(maxX - minX, maxY - minY); + const mapSpan = Math.min(viewBox.width, viewBox.height); + // Normalize every hovered territory toward the same visual footprint. Dense + // maps therefore expand dramatically while already-large sparse territories + // receive only a restrained lift. + const targetSpan = mapSpan * (dense ? 0.18 : 0.16); + const scale = Math.max(1.06, Math.min(8, targetSpan / territorySpan)); + return { "--mesh-hover-scale": scale } as React.CSSProperties; +} + +function edgesToPath( + edges: ReturnType, +): string { + return edges + .map( + (edge) => + `M ${edge.start.x} ${edge.start.y} L ${edge.end.x} ${edge.end.y}`, + ) + .join(" "); +} + +function EmptyHexes() { + const centers = [ + { x: 2, y: 1.75 }, + { x: 3.58, y: 2.65 }, + { x: 2, y: 3.55 }, + { x: 0.42, y: 2.65 }, + ]; + return ( + + ); +} + +function axialToPoint(q: number, r: number): Point { + return { x: SQRT_THREE * (q + r / 2), y: 1.5 * r }; +} + +function hexPoints(center: Point, radius: number): string { + return Array.from({ length: 6 }, (_, index) => { + const angle = ((60 * index - 30) * Math.PI) / 180; + return `${center.x + radius * Math.cos(angle)},${center.y + radius * Math.sin(angle)}`; + }).join(" "); +} + +function computeViewBox( + cells: readonly CommunityComputeHexCell[], +): ViewBox | null { + if (cells.length === 0) return null; + const centers = cells.map((cell) => axialToPoint(cell.q, cell.r)); + const minX = Math.min(...centers.map(({ x }) => x)) - 1.4; + const maxX = Math.max(...centers.map(({ x }) => x)) + 1.4; + const minY = Math.min(...centers.map(({ y }) => y)) - 1.4; + const maxY = Math.max(...centers.map(({ y }) => y)) + 1.4; + return { + x: minX, + y: minY, + width: Math.max(2.8, maxX - minX), + height: Math.max(2.8, maxY - minY), + }; +} + +function shortModelName(modelId: string): string { + const tail = modelId.split("/").at(-1) ?? modelId; + return tail.length > 34 ? `${tail.slice(0, 31)}…` : tail; +} + +function territoryAccessibleLabel( + deployment: CommunityComputeDeployment, +): string { + const capacity = formatCapacity(deployment.capacityGb); + return `${deployment.modelId} on ${deployment.deviceLabel}, ${capacity}`; +} + +function formatCapacity(value: number | null): string { + return value === null + ? "Capacity not reported" + : `${Math.round(value)} GB shared`; +} + +function testId(value: string): string { + return encodeURIComponent(value).replaceAll("%", "-"); +} diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx index c27c5eef3ac..bb5590b3e22 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx @@ -1,6 +1,5 @@ import * as React from "react"; -import { ChevronDown } from "lucide-react"; -import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { ChevronDown, Hexagon, Sparkles } from "lucide-react"; import { Switch } from "@/shared/ui/switch"; import { cn } from "@/shared/lib/cn"; @@ -29,6 +28,12 @@ import type { } from "@/shared/api/tauriMesh"; import { SettingsOptionGroup } from "@/features/settings/ui/SettingsOptionGroup"; import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader"; +import { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent"; +import { useCommunities } from "@/features/communities/useCommunities"; +import { Button } from "@/shared/ui/button"; +import { useMeshSnapshot } from "../hooks/useMeshSnapshot"; +import { deriveCommunityComputeMapModel } from "../communityComputeMapModel"; +import { CommunityComputeTerritoryMap } from "./CommunityComputeTerritoryMap"; import { classifyModelRef } from "../classifyModelRef"; import { downloadPercent, @@ -52,11 +57,6 @@ const MESH_SELECT_TRIGGER_CLASS = cn( "h-11 px-3 py-2 leading-6 hover:bg-muted/40 focus:bg-muted/40 [&>svg]:text-muted-foreground/60", ); -const SHARE_COMPUTE_REVEAL_TRANSITION = { - duration: 0.22, - ease: [0.23, 1, 0.32, 1], -} as const; - function readDraft(key: string): string { try { return window.localStorage.getItem(key) ?? ""; @@ -85,8 +85,11 @@ function writeDraft(key: string, value: string): void { * exposing implementation protocols or raw mesh controls. */ export function MeshComputeSettingsCard() { - const shouldReduceMotion = useReducedMotion(); - const { status, error, refresh } = useMeshNodeStatus(); + const { activeCommunity } = useCommunities(); + const communityName = activeCommunity?.name ?? "this community"; + const { status, error, refresh, update } = useMeshNodeStatus(); + const [snapshotRefreshKey, setSnapshotRefreshKey] = React.useState(0); + const { snapshot } = useMeshSnapshot({ refreshKey: snapshotRefreshKey }); const [installedModels, setInstalledModels] = React.useState< MeshModelOption[] >([]); @@ -187,6 +190,17 @@ export function MeshComputeSettingsCard() { const refClass = classifyModelRef(modelInput); const canStart = refClass.kind !== "unknown" && !actionInFlight; const showSharingControls = isSharing || pendingAction === "start"; + // Live runtime state wins over the still-pending command promise. Starting a + // node waits on an additional inference-readiness probe, so the command can + // remain in flight after status already proves this machine is running. Do + // not leave the visual state spinning during that window. + const isSuccessfullySharing = + status?.mode === "serve" && status.state === "running"; + const isTransitioning = + !isSuccessfullySharing && + (pendingAction !== null || + status?.state === "starting" || + status?.state === "stopping"); async function handleToggle(next: boolean) { // Never let the Share switch tear down a consume session. The switch is @@ -203,7 +217,7 @@ export function MeshComputeSettingsCard() { if (next) { const maxVram = maxVramGb.trim() === "" ? undefined : Number.parseFloat(maxVramGb); - await meshStartNode({ + const nextStatus = await meshStartNode({ mode: "serve", modelId: modelInput.trim() || undefined, maxVramGb: @@ -211,10 +225,13 @@ export function MeshComputeSettingsCard() { ? maxVram : undefined, }); + update(nextStatus); } else { - await meshStopNode(); + const nextStatus = await meshStopNode(); + update(nextStatus); } refresh(); + setSnapshotRefreshKey((current) => current + 1); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); } finally { @@ -227,8 +244,8 @@ export function MeshComputeSettingsCard() { return (
{error ? ( @@ -248,20 +265,68 @@ export function MeshComputeSettingsCard() {
-
- - {!isSharing ? ( - - ) : null} + {isSuccessfullySharing ? ( + + + + ) : null} + {isTransitioning ? ( + + Preparing shared compute + + + + ) : ( + + )} + +
+ + {!isSuccessfullySharing ? ( + + ) : ( +

+ This machine is actively sharing compute on {communityName} + ’s mesh. +

+ )} +
+ {showSharingControls ? ( +
+

Status

+
+ + {servingIndicator.show ? ( +

+ {servingIndicator.label} + {servingIndicator.detail + ? ` · ${servingIndicator.detail}` + : null} +

+ ) : null} +
+
+ ) : null} {status?.consoleUrl ? (

Debug console:{" "} @@ -347,59 +439,47 @@ export function MeshComputeSettingsCard() { ) : null}

- - {showSharingControls ? ( - -
-

Status

-
- - {servingIndicator.show ? ( -

- {servingIndicator.label} - {servingIndicator.detail ? ( - - {" "} - · {servingIndicator.detail} - - ) : null} -

- ) : null} -
-
-
- ) : null} -
+ {(snapshot?.contributorMemberCount ?? 0) >= 6 ? ( +
+
+

Community mesh

+

+ {snapshot?.contributorMemberCount} people are contributing + compute. +

+
+ +
+ ) : null}
+ +
+
+
+

+ Put the community mesh to work +

+

+ Anyone can use compute shared by the community—even if this + machine can’t contribute. Name your agent and customize its + instructions; we’ll handle the compute setup. +

+
+ +
+
); } @@ -496,7 +576,9 @@ function MeshModelPicker({ seen.add(entry.name); return { disabled: entry.fit === "too_large", - label: , + label: ( + + ), value: entry.name, }; }); @@ -523,7 +605,7 @@ function MeshModelPicker({ ...localOptions, { label: "Custom model…", value: CUSTOM_MODEL_DROPDOWN_VALUE }, ]; - }, [catalog?.entries, installedModels]); + }, [catalog?.entries, catalog?.gpuName, installedModels]); const knownModel = options.some((option) => option.value === model.trim()); const showCustomModelInput = isCustomModelEditing || (model.trim().length > 0 && !knownModel); @@ -578,16 +660,19 @@ function MeshModelPicker({ className="text-sm font-normal text-muted-foreground/70" data-settings-subcopy > - {catalog - ? `Recommended for this machine${catalog.gpuName ? ` (${catalog.gpuName}, ${catalog.vramDisplay} AI memory)` : ""}.` - : "Choose a model or enter a model reference or local file."}{" "} - Buzz downloads remote models when sharing starts. + Buzz downloads the selected model when sharing starts.

); } -function MeshModelOptionLabel({ entry }: { entry: MeshCatalogEntry }) { +function MeshModelOptionLabel({ + entry, + hardware, +}: { + entry: MeshCatalogEntry; + hardware?: string | null; +}) { return (
{entry.name} @@ -596,8 +681,8 @@ function MeshModelOptionLabel({ entry }: { entry: MeshCatalogEntry }) { {FIT_LABEL[entry.fit]} {entry.recommended ? ( - - Recommended + + Recommended for {hardware ?? "this machine"} ) : null} {entry.installed ? ( diff --git a/desktop/src/shared/api/tauriMesh.ts b/desktop/src/shared/api/tauriMesh.ts index 8a0153123f4..1890e766501 100644 --- a/desktop/src/shared/api/tauriMesh.ts +++ b/desktop/src/shared/api/tauriMesh.ts @@ -112,3 +112,35 @@ export type MeshModelCatalog = { export async function meshModelCatalog(): Promise { return await invokeTauri("mesh_model_catalog"); } + +export type MeshDeviceState = "serving" | "loading" | "standby" | "consuming"; + +export type MeshSnapshotDevice = { + deviceId: string | null; + label: string; + capacityGb: number | null; + models: string[]; + state: MeshDeviceState; + isSelf: boolean; + memberPubkey?: string | null; + reportedAt?: number | null; + modelSizeGb?: number | null; +}; + +export type MeshSnapshot = { + sharingDeviceCount: number; + contributorMemberCount?: number; + sharedCapacityGb: number | null; + allocatedCapacityGb?: number | null; + models: string[]; + devices: MeshSnapshotDevice[]; + includesSelf: boolean; + observedAt?: number; + freshnessSeconds?: number; + memberCount: number; + reason: string | null; +}; + +export async function meshSnapshot(): Promise { + return await invokeTauri("mesh_snapshot"); +} diff --git a/desktop/src/shared/styles/globals/animations.css b/desktop/src/shared/styles/globals/animations.css index 5a0723cdaa0..bc248a05231 100644 --- a/desktop/src/shared/styles/globals/animations.css +++ b/desktop/src/shared/styles/globals/animations.css @@ -930,3 +930,14 @@ animation: none; } } + +/* The shape stays fixed while a short dark stroke travels around its edge. */ +@keyframes mesh-compute-hex-trace { + to { + stroke-dashoffset: -100; + } +} + +.mesh-compute-hex-trace { + animation: mesh-compute-hex-trace 1.1s linear infinite; +} From 852d4219038fc4d8d1115ae9681ab5ea628ad175 Mon Sep 17 00:00:00 2001 From: Alessandro Joabar Date: Fri, 28 Aug 2026 23:49:05 -0400 Subject: [PATCH 20/20] Update shared compute smoke coverage Signed-off-by: Alessandro Joabar --- desktop/tests/e2e/mesh-compute.spec.ts | 28 ++++++++++---------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/desktop/tests/e2e/mesh-compute.spec.ts b/desktop/tests/e2e/mesh-compute.spec.ts index c55b9db5d95..e7dd937d4b8 100644 --- a/desktop/tests/e2e/mesh-compute.spec.ts +++ b/desktop/tests/e2e/mesh-compute.spec.ts @@ -26,9 +26,6 @@ test("Share compute chooses a model before sharing", async ({ page }) => { const model = page.getByTestId("mesh-share-compute-model"); await expect(card).not.toContainText("Not sharing right now"); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toHaveCount(0); await expect( page.getByTestId("mesh-share-compute-sharing-status"), ).toHaveCount(0); @@ -39,17 +36,20 @@ test("Share compute chooses a model before sharing", async ({ page }) => { await page.getByLabel("Custom model reference").fill(modelRef); await toggle.click(); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toBeVisible(); - await expect( - page.getByTestId("mesh-share-compute-sharing-status"), - ).toBeVisible(); + await expect(toggle).toBeChecked(); + await expect(card).toContainText("You’re sharing compute"); + await expect(card).toContainText("This machine is actively sharing compute"); await expect(model).toBeVisible(); await expect(card).toContainText( - "Buzz downloads remote models when sharing starts", + "Buzz downloads the selected model when sharing starts", ); - await expect(toggle).toBeChecked(); + + // Runtime/model details are intentionally secondary to the successful + // sharing state and remain available under Advanced. + await page.getByTestId("mesh-share-compute-advanced-toggle").click(); + await expect( + page.getByTestId("mesh-share-compute-sharing-status"), + ).toBeVisible(); await expect( page.getByTestId("mesh-share-compute-sharing-status"), ).toContainText("SmolLM2 135M with relay members"); @@ -74,9 +74,6 @@ test("Share compute chooses a model before sharing", async ({ page }) => { await toggle.click(); await expect(toggle).not.toBeChecked(); await expect(card).not.toContainText("Not sharing right now"); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toHaveCount(0); await expect( page.getByTestId("mesh-share-compute-sharing-status"), ).toHaveCount(0); @@ -123,9 +120,6 @@ test("a consuming client can switch to sharing its saved local model", async ({ ); await expect(card).toContainText("Buzz may briefly restart"); await expect(toggle).not.toBeChecked(); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toHaveCount(0); await expect(toggle).toBeEnabled(); const customModel = page.getByLabel("Custom model reference"); await expect(customModel).toHaveValue(localModel);