diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 25a59c32432..3eec4dc2e84 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 feature 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/Cargo.lock b/Cargo.lock
index 9544a63b899..7af80993abf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -799,7 +799,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",
@@ -1574,9 +1574,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"
@@ -2786,6 +2786,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"
@@ -3333,6 +3345,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"
@@ -4007,9 +4028,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",
@@ -4058,9 +4079,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",
@@ -4077,9 +4098,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",
@@ -4128,9 +4149,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",
@@ -4167,7 +4188,6 @@ dependencies = [
"tokio-websockets",
"tracing",
"url",
- "vergen-gitcl",
"webpki-roots 1.0.7",
"ws_stream_wasm",
]
@@ -4289,16 +4309,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"
@@ -4517,10 +4527,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",
]
@@ -4777,8 +4788,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"hex",
"mesh-llm-client",
@@ -4787,8 +4798,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-api-client",
@@ -4798,21 +4809,18 @@ 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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
[[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-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",
@@ -4822,11 +4830,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",
@@ -4835,8 +4841,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -4851,8 +4857,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-host-runtime",
@@ -4861,20 +4867,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"serde",
"serde_json",
@@ -4883,8 +4894,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"serde",
"serde_json",
@@ -4892,8 +4903,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"mesh-llm-native-runtime",
]
@@ -4929,8 +4940,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"argon2",
@@ -4952,7 +4963,6 @@ dependencies = [
"http-body-util",
"httparse",
"iroh",
- "json5",
"keyring",
"libc",
"mdns-sd",
@@ -4964,6 +4974,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",
@@ -4997,7 +5008,6 @@ dependencies = [
"semver",
"serde",
"serde_json",
- "serde_yaml",
"sha2 0.10.9",
"skippy-coordinator",
"skippy-ffi",
@@ -5017,14 +5027,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"argon2",
"base64 0.22.1",
@@ -5043,10 +5055,28 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "mesh-llm-log-store"
+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",
+ "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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"serde",
@@ -5056,8 +5086,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-types",
@@ -5070,8 +5100,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"async-trait",
@@ -5087,8 +5117,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5106,8 +5136,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"hex",
@@ -5119,8 +5149,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"hex",
"sha2 0.10.9",
@@ -5128,16 +5158,17 @@ 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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+version = "0.76.0-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5159,8 +5190,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-api-client",
@@ -5174,8 +5205,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5185,8 +5216,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"chrono",
@@ -5212,8 +5243,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"hex",
"serde",
@@ -5223,13 +5254,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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -5242,18 +5273,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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+version = "0.76.0-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"libloading",
"mesh-native-serving-plugin-api",
"skippy-server",
+ "skippy-tokenizer",
]
[[package]]
@@ -5393,8 +5425,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"async-trait",
@@ -5404,8 +5436,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"async-trait",
@@ -5422,8 +5454,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"bytes",
@@ -5442,16 +5474,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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -5802,9 +5834,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",
@@ -5824,9 +5856,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",
@@ -5852,9 +5884,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",
@@ -6252,19 +6284,20 @@ 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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"async-trait",
"axum",
"futures-core",
"futures-util",
+ "mesh-llm-events",
"mesh-llm-guardrails",
"serde",
"serde_json",
"tokio",
- "tokio-stream",
"tracing",
+ "uuid",
]
[[package]]
@@ -7994,6 +8027,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"
@@ -8782,8 +8829,8 @@ 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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"blake3",
@@ -8792,29 +8839,29 @@ dependencies = [
[[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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -8825,8 +8872,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"libc",
@@ -8839,8 +8886,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"ahash",
"anyhow",
@@ -8851,6 +8898,7 @@ dependencies = [
"clap",
"futures-util",
"libc",
+ "mesh-llm-events",
"mesh-native-serving-plugin-api",
"model-artifact",
"openai-frontend",
@@ -8863,24 +8911,25 @@ 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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -9017,7 +9066,7 @@ dependencies = [
"futures-io",
"futures-util",
"hashbrown 0.16.1",
- "hashlink",
+ "hashlink 0.11.0",
"indexmap",
"log",
"memchr",
@@ -10544,43 +10593,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"
diff --git a/Justfile b/Justfile
index 32d83355e1c..4df797071ab 100644
--- a/Justfile
+++ b/Justfile
@@ -210,8 +210,13 @@ _ensure-migrations: _ensure-services
./scripts/seed-local-community.sh
# Run clippy on the desktop Tauri Rust crate
+# 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
desktop-tauri-check: _ensure-sidecar-stubs
@@ -611,7 +616,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
diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml
index deb2e7e16a5..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.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-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 68b702431af..44f85b6db0f 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]]
@@ -1475,9 +1475,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"
@@ -1640,7 +1640,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]]
@@ -2307,7 +2307,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]]
@@ -2485,7 +2485,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2766,7 +2766,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]]
@@ -3284,7 +3284,7 @@ dependencies = [
"libc",
"log",
"rustversion",
- "windows-link 0.1.3",
+ "windows-link 0.2.1",
"windows-result 0.4.1",
]
@@ -4358,14 +4358,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",
@@ -4409,9 +4409,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",
@@ -4428,12 +4428,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",
@@ -4479,13 +4479,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",
@@ -4518,7 +4518,6 @@ dependencies = [
"tokio-websockets",
"tracing",
"url",
- "vergen-gitcl",
"webpki-roots 1.0.8",
"ws_stream_wasm",
]
@@ -4693,16 +4692,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"
@@ -5176,8 +5165,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"hex",
"mesh-llm-client",
@@ -5186,8 +5175,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-api-client",
@@ -5197,21 +5186,18 @@ 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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
[[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-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",
@@ -5221,11 +5207,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",
@@ -5234,8 +5218,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5250,8 +5234,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-host-runtime",
@@ -5260,20 +5244,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"serde",
"serde_json",
@@ -5282,8 +5271,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"serde",
"serde_json",
@@ -5291,8 +5280,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"mesh-llm-native-runtime",
]
@@ -5328,8 +5317,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"argon2",
@@ -5351,7 +5340,6 @@ dependencies = [
"http-body-util",
"httparse",
"iroh",
- "json5",
"keyring",
"libc",
"mdns-sd",
@@ -5363,6 +5351,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",
@@ -5396,7 +5385,6 @@ dependencies = [
"semver",
"serde",
"serde_json",
- "serde_yaml",
"sha2 0.10.9",
"skippy-coordinator",
"skippy-ffi",
@@ -5416,14 +5404,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"argon2",
"base64 0.22.1",
@@ -5442,10 +5432,28 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "mesh-llm-log-store"
+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",
+ "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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"serde",
@@ -5455,8 +5463,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-types",
@@ -5469,8 +5477,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"async-trait",
@@ -5486,8 +5494,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5505,8 +5513,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"hex",
@@ -5518,8 +5526,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"hex",
"sha2 0.10.9",
@@ -5527,16 +5535,17 @@ 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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+version = "0.76.0-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5558,8 +5567,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"mesh-llm-api-client",
@@ -5573,8 +5582,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"dirs",
@@ -5584,8 +5593,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"chrono",
@@ -5611,8 +5620,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"hex",
"serde",
@@ -5622,13 +5631,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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -5641,18 +5650,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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
"mesh-native-serving-plugin-api",
"skippy-server",
+ "skippy-tokenizer",
]
[[package]]
@@ -5742,13 +5752,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"async-trait",
@@ -5758,8 +5768,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"async-trait",
@@ -5776,8 +5786,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"bytes",
@@ -5796,16 +5806,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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -5901,7 +5911,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.18",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5937,7 +5947,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",
@@ -6127,7 +6137,7 @@ checksum = "4d9cbe01741347ef750d743d6690603f5eed8341e679fb51c8e629337aa11976"
dependencies = [
"atomic-waker",
"bytes",
- "cfg_aliases 0.2.1",
+ "cfg_aliases 0.2.2",
"derive_more",
"ipnet",
"js-sys",
@@ -6182,7 +6192,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",
]
@@ -6195,7 +6205,7 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.13.0",
"cfg-if 1.0.4",
- "cfg_aliases 0.2.1",
+ "cfg_aliases 0.2.2",
"libc",
]
@@ -6207,7 +6217,7 @@ checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.13.0",
"cfg-if 1.0.4",
- "cfg_aliases 0.2.1",
+ "cfg_aliases 0.2.2",
"libc",
]
@@ -6232,12 +6242,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",
@@ -6254,9 +6264,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",
@@ -6282,11 +6292,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",
@@ -6414,7 +6424,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]]
@@ -6524,7 +6534,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 2.0.2",
"proc-macro2",
"quote",
"syn 2.0.118",
@@ -6906,19 +6916,20 @@ 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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"async-trait",
"axum",
"futures-core",
"futures-util",
+ "mesh-llm-events",
"mesh-llm-guardrails",
"serde",
"serde_json",
"tokio",
- "tokio-stream",
"tracing",
+ "uuid",
]
[[package]]
@@ -7118,7 +7129,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]]
@@ -8097,7 +8108,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",
@@ -8139,12 +8150,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]]
@@ -8875,7 +8886,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -8945,7 +8956,7 @@ dependencies = [
"security-framework 3.7.0",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -9212,7 +9223,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]]
@@ -9697,8 +9708,8 @@ 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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"blake3",
@@ -9707,29 +9718,29 @@ dependencies = [
[[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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -9740,8 +9751,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"anyhow",
"libc",
@@ -9754,8 +9765,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-rc7"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.76.0-rc7#5d07a20b674dfa4cd3d86f6721e8c5b2284d1d23"
dependencies = [
"ahash",
"anyhow",
@@ -9766,6 +9777,7 @@ dependencies = [
"clap",
"futures-util",
"libc",
+ "mesh-llm-events",
"mesh-native-serving-plugin-api",
"model-artifact",
"openai-frontend",
@@ -9778,24 +9790,25 @@ 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-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.75.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.75.1#3295c902d4c4f859aaadf9240042ffdaf06dd07e"
+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",
@@ -9831,7 +9844,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]]
@@ -10856,10 +10869,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]]
@@ -10881,7 +10894,7 @@ dependencies = [
"parking_lot",
"rustix 1.1.4",
"signal-hook 0.3.18",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -11623,7 +11636,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.18",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -11724,7 +11737,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -11987,43 +12000,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"
@@ -12575,7 +12551,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 f41fa2d6e39..5735426d28e 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-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.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-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 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/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/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/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/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();
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 1a11fcfcd13..00cd8ea8fa4 100644
--- a/desktop/src-tauri/src/mesh_llm/catalog.rs
+++ b/desktop/src-tauri/src/mesh_llm/catalog.rs
@@ -13,38 +13,49 @@ 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 recommended ladder follows rated unified memory:
+/// - below 32 GB: Gemma 4 E4B;
+/// - 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.
+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";
-/// 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;
+/// 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 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.
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,
}
}
-/// 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_MEDIUM_ALIAS => CURATED_MEDIUM,
CURATED_LARGE_ALIAS => CURATED_LARGE,
+ LEGACY_LARGE_ALIAS => LEGACY_LARGE,
other => other,
}
}
@@ -172,31 +183,14 @@ 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());
- // 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| {
@@ -269,20 +263,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_LARGE, "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M");
- // 64GB+ rated machines get the large curated pick.
- let large = build_catalog(None, 64_000_000_000, 64.0, &[]);
+ 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 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, &[]);
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]
@@ -291,10 +293,15 @@ 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
);
+ assert_eq!(canonical_curated_model_id(LEGACY_LARGE_ALIAS), LEGACY_LARGE);
assert_eq!(
canonical_curated_model_id("other/model:Q4"),
"other/model:Q4"
@@ -304,14 +311,17 @@ 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));
- // The synthesized large pick carries a real size for fit ranking.
+ 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);
}
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 (
+
+ {centers.map((center) => (
+
+ ))}
+
+ );
+}
+
+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() {
-
-
+
- Share this machine
-
- {!isSharing ? (
-
- ) : null}
+ {isSuccessfullySharing ? (
+
+
+
+ ) : null}
+ {isTransitioning ? (
+
+ Preparing shared compute
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {isSuccessfullySharing
+ ? "You’re sharing compute"
+ : "Share your machine"}
+
+ {!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.
+
+
+
requestOpenCreateAgent({ preset: "community-mesh" })}
+ type="button"
+ >
+ Create community agent
+
+
+
);
}
@@ -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;
+}
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);