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

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

+ )} +
+ {showSharingControls ? ( +
+

Status

+
+ + {servingIndicator.show ? ( +

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

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

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

- - {showSharingControls ? ( - -
-

Status

-
- - {servingIndicator.show ? ( -

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

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

Community mesh

+

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

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

+ Put the community mesh to work +

+

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

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

); } -function MeshModelOptionLabel({ entry }: { entry: MeshCatalogEntry }) { +function MeshModelOptionLabel({ + entry, + hardware, +}: { + entry: MeshCatalogEntry; + hardware?: string | null; +}) { return (
{entry.name} @@ -596,8 +681,8 @@ function MeshModelOptionLabel({ entry }: { entry: MeshCatalogEntry }) { {FIT_LABEL[entry.fit]} {entry.recommended ? ( - - Recommended + + Recommended for {hardware ?? "this machine"} ) : null} {entry.installed ? ( diff --git a/desktop/src/shared/api/tauriMesh.ts b/desktop/src/shared/api/tauriMesh.ts index 8a0153123f4..1890e766501 100644 --- a/desktop/src/shared/api/tauriMesh.ts +++ b/desktop/src/shared/api/tauriMesh.ts @@ -112,3 +112,35 @@ export type MeshModelCatalog = { export async function meshModelCatalog(): Promise { return await invokeTauri("mesh_model_catalog"); } + +export type MeshDeviceState = "serving" | "loading" | "standby" | "consuming"; + +export type MeshSnapshotDevice = { + deviceId: string | null; + label: string; + capacityGb: number | null; + models: string[]; + state: MeshDeviceState; + isSelf: boolean; + memberPubkey?: string | null; + reportedAt?: number | null; + modelSizeGb?: number | null; +}; + +export type MeshSnapshot = { + sharingDeviceCount: number; + contributorMemberCount?: number; + sharedCapacityGb: number | null; + allocatedCapacityGb?: number | null; + models: string[]; + devices: MeshSnapshotDevice[]; + includesSelf: boolean; + observedAt?: number; + freshnessSeconds?: number; + memberCount: number; + reason: string | null; +}; + +export async function meshSnapshot(): Promise { + return await invokeTauri("mesh_snapshot"); +} diff --git a/desktop/src/shared/styles/globals/animations.css b/desktop/src/shared/styles/globals/animations.css index 5a0723cdaa0..bc248a05231 100644 --- a/desktop/src/shared/styles/globals/animations.css +++ b/desktop/src/shared/styles/globals/animations.css @@ -930,3 +930,14 @@ animation: none; } } + +/* The shape stays fixed while a short dark stroke travels around its edge. */ +@keyframes mesh-compute-hex-trace { + to { + stroke-dashoffset: -100; + } +} + +.mesh-compute-hex-trace { + animation: mesh-compute-hex-trace 1.1s linear infinite; +} 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);