mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): shared-compute usability — share toggle, usage indicator, model resync (#2448)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Michael Neale
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
Tyler Longwell
parent
c86c4f59c4
commit
9cc9652c7d
Generated
+50
-50
@@ -1505,7 +1505,7 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1742,7 +1742,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.62.2",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3090,8 +3090,8 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3836,7 +3836,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry 0.6.1",
|
||||
"windows-registry 0.5.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3851,7 +3851,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4928,7 +4928,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-api-client"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"mesh-llm-client",
|
||||
@@ -4938,7 +4938,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-api-server"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"mesh-llm-api-client",
|
||||
@@ -4949,12 +4949,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-build-info"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
|
||||
[[package]]
|
||||
name = "mesh-llm-client"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4986,7 +4986,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-config"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs",
|
||||
@@ -5002,7 +5002,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-embedded-runtime"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"mesh-llm-host-runtime",
|
||||
@@ -5012,7 +5012,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-events"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -5024,7 +5024,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-gpu-bench"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
@@ -5037,7 +5037,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-guardrails"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5046,7 +5046,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-hardware-profile"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"mesh-llm-native-runtime",
|
||||
]
|
||||
@@ -5054,7 +5054,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-host-runtime"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -5147,7 +5147,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-identity"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64 0.22.1",
|
||||
@@ -5169,7 +5169,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-native-runtime"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -5180,7 +5180,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-node"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"mesh-llm-types",
|
||||
@@ -5194,7 +5194,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-plugin"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5211,7 +5211,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-plugin-manager"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs",
|
||||
@@ -5229,7 +5229,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-protocol"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hex",
|
||||
@@ -5242,7 +5242,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-routing"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"iroh",
|
||||
]
|
||||
@@ -5250,7 +5250,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-runtime-install"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs",
|
||||
@@ -5273,7 +5273,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-sdk"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"mesh-llm-api-client",
|
||||
@@ -5288,7 +5288,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-skills"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs",
|
||||
@@ -5299,7 +5299,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-system"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -5322,7 +5322,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-types"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"serde",
|
||||
@@ -5333,12 +5333,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mesh-llm-ui"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
|
||||
[[package]]
|
||||
name = "mesh-mixture-of-agents"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"mesh-llm-guardrails",
|
||||
@@ -5411,7 +5411,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "model-artifact"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5422,7 +5422,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "model-hf"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5440,7 +5440,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "model-package"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5460,7 +5460,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "model-ref"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5468,7 +5468,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "model-resolver"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"model-artifact",
|
||||
@@ -6113,7 +6113,7 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
@@ -6488,7 +6488,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "openai-frontend"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -7381,7 +7381,7 @@ version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"heck 0.4.1",
|
||||
"itertools",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -9011,7 +9011,7 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
[[package]]
|
||||
name = "skippy-cache"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"blake3",
|
||||
@@ -9021,7 +9021,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "skippy-coordinator"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
@@ -9029,7 +9029,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "skippy-ffi"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
@@ -9037,12 +9037,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "skippy-metrics"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
|
||||
[[package]]
|
||||
name = "skippy-protocol"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"prost",
|
||||
"prost-build",
|
||||
@@ -9053,7 +9053,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "skippy-runtime"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
@@ -9067,7 +9067,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "skippy-server"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -9095,7 +9095,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "skippy-topology"
|
||||
version = "0.73.1"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
|
||||
source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=c441ea7f328692b18b7f49ad819c7b1a603cbdcb#c441ea7f328692b18b7f49ad819c7b1a603cbdcb"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -11677,7 +11677,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12288,8 +12288,8 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -90,14 +90,14 @@ buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona
|
||||
buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
|
||||
buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" }
|
||||
iroh = { version = "1.0.2", optional = true }
|
||||
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.73.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.73.1", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
|
||||
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
|
||||
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
|
||||
# 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.73.1", package = "mesh-llm-client", optional = true }
|
||||
mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.73.1", package = "mesh-llm-node", optional = true }
|
||||
mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.73.1", package = "mesh-llm-system", optional = true }
|
||||
mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.73.1", package = "mesh-llm-events", optional = true }
|
||||
mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-client", optional = true }
|
||||
mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-node", optional = true }
|
||||
mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-system", optional = true }
|
||||
mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "c441ea7f328692b18b7f49ad819c7b1a603cbdcb", package = "mesh-llm-events", optional = true }
|
||||
base64 = "0.22"
|
||||
sha2 = "0.11"
|
||||
tar = "0.4"
|
||||
|
||||
@@ -45,6 +45,18 @@ fn load_mesh_sharing_config(app: &AppHandle) -> Result<Option<MeshSharingConfig>
|
||||
const RELAY_MESH_RUNTIME_NO_TARGET: &str =
|
||||
"Buzz shared compute requires a live serving member; start serving the selected model on a member, then try again";
|
||||
|
||||
/// Whether the Share-compute "stop sharing" path (`mesh_stop_node`) should tear
|
||||
/// down the runtime currently occupying the single slot.
|
||||
///
|
||||
/// Serve nodes (this machine SHARING compute) are torn down. Client nodes (this
|
||||
/// machine CONSUMING a peer's compute) share the same slot and MUST be left
|
||||
/// running — stopping "Share compute" must never kill a consume session the
|
||||
/// user didn't start from this switch.
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
fn share_stop_should_teardown(mode: mesh_llm::MeshNodeMode) -> bool {
|
||||
matches!(mode, mesh_llm::MeshNodeMode::Serve)
|
||||
}
|
||||
|
||||
pub type CmdResult<T> = Result<T, String>;
|
||||
|
||||
fn advance_mesh_status_cursor(
|
||||
@@ -197,19 +209,110 @@ pub async fn mesh_start_node(
|
||||
/// Mesh can bind its HTTP ingress and advertise a model shortly before the
|
||||
/// router has installed a usable target. Probe the exact chat path agents use
|
||||
/// so startup cannot race that gap (`single target None unavailable`).
|
||||
/// Which startup stage a mesh client is stuck at when it never becomes
|
||||
/// inference-ready. The two live-observed failure modes are physically
|
||||
/// distinct and want different user copy:
|
||||
///
|
||||
/// * `CatalogNeverSynced` — the local client node came up and connected to
|
||||
/// the host at the control level (ping/RTT fine), but the served model
|
||||
/// never appeared in the local `/v1/models` catalog. That catalog is
|
||||
/// populated by the peer gossip exchange; when the gossip bi-stream can't
|
||||
/// establish across the network (observed as iroh
|
||||
/// `MultipathNotNegotiated` / unreachable direct path), the catalog stays
|
||||
/// empty forever and every request is rejected "model not available".
|
||||
/// Root cause is the network path between this machine and the host.
|
||||
/// * `RoutingNeverCompleted` — the model *did* sync into the catalog, but
|
||||
/// inference requests never completed (routing/transport to the host
|
||||
/// failing per-request). The host is discoverable and advertised but not
|
||||
/// actually serving us.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum MeshReadinessFailure {
|
||||
CatalogNeverSynced,
|
||||
RoutingNeverCompleted,
|
||||
}
|
||||
|
||||
/// Pure classifier: given whether the served model was ever observed in the
|
||||
/// local `/v1/models` catalog during the wait, decide which stage failed.
|
||||
/// Split out so the diagnosis is unit-testable without a live mesh.
|
||||
fn classify_mesh_readiness_failure(model_ever_visible: bool) -> MeshReadinessFailure {
|
||||
if model_ever_visible {
|
||||
MeshReadinessFailure::RoutingNeverCompleted
|
||||
} else {
|
||||
MeshReadinessFailure::CatalogNeverSynced
|
||||
}
|
||||
}
|
||||
|
||||
/// Actionable, non-technical copy for a readiness failure. `last_detail` is the
|
||||
/// last raw transport/HTTP error, appended for support triage.
|
||||
fn mesh_readiness_failure_message(
|
||||
failure: MeshReadinessFailure,
|
||||
model_id: &str,
|
||||
last_detail: &str,
|
||||
) -> String {
|
||||
match failure {
|
||||
MeshReadinessFailure::CatalogNeverSynced => format!(
|
||||
"Buzz shared compute connected to the serving member but could not sync \
|
||||
the model list for \"{model_id}\" — this is a network path problem \
|
||||
between this machine and the host (the compute node is reachable for \
|
||||
pings but the model-sync stream did not establish). Try again, or have \
|
||||
the host and this machine on a more direct network. (last: {last_detail})"
|
||||
),
|
||||
MeshReadinessFailure::RoutingNeverCompleted => format!(
|
||||
"Buzz shared compute found \"{model_id}\" on a serving member but inference \
|
||||
requests did not complete — the host is discoverable but not currently \
|
||||
reachable for requests. Try again shortly. (last: {last_detail})"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll the local mesh OpenAI ingress until a real inference for `model_id`
|
||||
/// succeeds, or a deadline elapses. On failure, returns a stage-specific,
|
||||
/// actionable message (see [`MeshReadinessFailure`]) rather than a raw
|
||||
/// `HTTP 429`, so the UI can tell "still warming up" apart from "can't reach
|
||||
/// the host".
|
||||
async fn wait_for_mesh_inference(model_id: &str) -> CmdResult<()> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|error| format!("failed to build mesh readiness client: {error}"))?;
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(120);
|
||||
let models_url = format!("{}/models", crate::managed_agents::RELAY_MESH_API_BASE_URL);
|
||||
let chat_url = format!(
|
||||
"{}/chat/completions",
|
||||
crate::managed_agents::RELAY_MESH_API_BASE_URL
|
||||
);
|
||||
let mut last_error = "mesh inference is not ready".to_string();
|
||||
// Track whether the served model ever reached the local catalog — the
|
||||
// signal that splits "catalog never synced" from "routing never completed".
|
||||
let mut model_ever_visible = false;
|
||||
|
||||
while tokio::time::Instant::now() < deadline {
|
||||
// Refresh catalog visibility. "auto" delegates model choice to the
|
||||
// router, so any advertised model counts as the catalog having synced.
|
||||
if let Ok(response) = client
|
||||
.get(&models_url)
|
||||
.bearer_auth(crate::managed_agents::RELAY_MESH_API_KEY_PLACEHOLDER)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
if let Ok(body) = response.json::<serde_json::Value>().await {
|
||||
if let Some(data) = body.get("data").and_then(|d| d.as_array()) {
|
||||
let wanted = model_id.trim().replace("@main", "");
|
||||
let visible = !data.is_empty()
|
||||
&& (model_id == crate::mesh_llm::AUTO_MODEL_ID
|
||||
|| data.iter().any(|m| {
|
||||
m.get("id")
|
||||
.and_then(|id| id.as_str())
|
||||
.map(|id| id.replace("@main", "") == wanted)
|
||||
.unwrap_or(false)
|
||||
}));
|
||||
model_ever_visible |= visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match client
|
||||
.post(format!(
|
||||
"{}/chat/completions",
|
||||
crate::managed_agents::RELAY_MESH_API_BASE_URL
|
||||
))
|
||||
.post(&chat_url)
|
||||
.bearer_auth(crate::managed_agents::RELAY_MESH_API_KEY_PLACEHOLDER)
|
||||
.json(&serde_json::json!({
|
||||
"model": model_id,
|
||||
@@ -230,8 +333,12 @@ async fn wait_for_mesh_inference(model_id: &str) -> CmdResult<()> {
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
Err(format!(
|
||||
"Buzz shared compute did not become inference-ready for {model_id}: {last_error}"
|
||||
|
||||
let failure = classify_mesh_readiness_failure(model_ever_visible);
|
||||
Err(mesh_readiness_failure_message(
|
||||
failure,
|
||||
model_id,
|
||||
&last_error,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -411,8 +518,22 @@ pub async fn mesh_stop_node(
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> CmdResult<mesh_llm::MeshNodeStatus> {
|
||||
let runtime = state.mesh_llm_runtime.lock().await.take();
|
||||
if let Some(runtime) = runtime {
|
||||
// The single runtime slot is shared by serve (this machine SHARING
|
||||
// compute) and client (this machine CONSUMING a peer's compute) roles.
|
||||
// Stopping "Share compute" must NEVER tear down a client node: inspect the
|
||||
// role under the lock and, when it's a consume session, leave it running
|
||||
// and return its live status unchanged. The frontend also guards this, but
|
||||
// status can be stale between polls, so the backend is authoritative.
|
||||
let taken = {
|
||||
let mut guard = state.mesh_llm_runtime.lock().await;
|
||||
if let Some(runtime) = guard.as_ref() {
|
||||
if !share_stop_should_teardown(runtime.mode()) {
|
||||
return runtime.status().await.map_err(|error| error.to_string());
|
||||
}
|
||||
}
|
||||
guard.take()
|
||||
};
|
||||
if let Some(runtime) = taken {
|
||||
runtime.stop().await.map_err(|error| error.to_string())?;
|
||||
}
|
||||
save_mesh_sharing_config(
|
||||
@@ -436,6 +557,20 @@ pub async fn mesh_node_status(state: State<'_, AppState>) -> CmdResult<mesh_llm:
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only host-side usage: who/what is using the compute this machine is
|
||||
/// sharing. Returns a zeroed snapshot when no runtime is active. No new trust
|
||||
/// surface — it reads the serving node's own runtime metrics.
|
||||
#[tauri::command]
|
||||
pub async fn mesh_serving_usage(
|
||||
state: State<'_, AppState>,
|
||||
) -> CmdResult<mesh_llm::MeshServingUsage> {
|
||||
let runtime = state.mesh_llm_runtime.lock().await;
|
||||
match runtime.as_ref() {
|
||||
Some(runtime) => runtime.serving_usage().await.map_err(|e| e.to_string()),
|
||||
None => Ok(mesh_llm::MeshServingUsage::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_installed_models(
|
||||
state: State<'_, AppState>,
|
||||
@@ -479,6 +614,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readiness_failure_is_catalog_sync_when_model_never_visible() {
|
||||
assert_eq!(
|
||||
classify_mesh_readiness_failure(false),
|
||||
MeshReadinessFailure::CatalogNeverSynced
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readiness_failure_is_routing_when_model_was_visible() {
|
||||
assert_eq!(
|
||||
classify_mesh_readiness_failure(true),
|
||||
MeshReadinessFailure::RoutingNeverCompleted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readiness_messages_are_distinct_and_actionable() {
|
||||
let catalog = mesh_readiness_failure_message(
|
||||
MeshReadinessFailure::CatalogNeverSynced,
|
||||
"auto",
|
||||
"HTTP 429",
|
||||
);
|
||||
let routing = mesh_readiness_failure_message(
|
||||
MeshReadinessFailure::RoutingNeverCompleted,
|
||||
"auto",
|
||||
"HTTP 503",
|
||||
);
|
||||
// Distinct diagnoses, each names the model and carries the raw detail.
|
||||
assert_ne!(catalog, routing);
|
||||
assert!(catalog.contains("network path"));
|
||||
assert!(catalog.contains("HTTP 429"));
|
||||
assert!(routing.contains("did not complete"));
|
||||
assert!(routing.contains("HTTP 503"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_status_cursor_uses_relay_composite_tiebreak() {
|
||||
let event = nostr::EventBuilder::new(nostr::Kind::TextNote, "status")
|
||||
@@ -546,6 +717,52 @@ mod tests {
|
||||
assert_eq!(pick_serve_target_for_model(targets, "model-missing"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_stop_tears_down_serve_but_not_client() {
|
||||
// Stopping "Share compute" tears down a serve node (we were sharing)
|
||||
// but must leave a client node alone (we are consuming a peer). This is
|
||||
// the backend half of the toggle-on regression: a client node occupies
|
||||
// the single slot and reports state:"running", and the stop path must
|
||||
// not kill it.
|
||||
assert!(
|
||||
share_stop_should_teardown(mesh_llm::MeshNodeMode::Serve),
|
||||
"serve node is our sharing runtime; stop must tear it down"
|
||||
);
|
||||
assert!(
|
||||
!share_stop_should_teardown(mesh_llm::MeshNodeMode::Client),
|
||||
"client node is a consume session; stop must NOT tear it down"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_status_serializes_with_running_state_and_client_mode() {
|
||||
// Contract pin for the TS mock (e2eBridge.ts) and the frontend
|
||||
// predicate: a consuming node serializes as
|
||||
// {"state":"running","mode":"client"}. If serde renaming drifts, the
|
||||
// hand-written mock shape and `deriveMeshShareToggle` would silently
|
||||
// stop matching the real IPC payload.
|
||||
let status = mesh_llm::MeshNodeStatus {
|
||||
state: mesh_llm::MeshNodeState::Running,
|
||||
mode: Some(mesh_llm::MeshNodeMode::Client),
|
||||
// `MeshHealth::ok()` is module-private; build via the public fields.
|
||||
health: mesh_llm::MeshHealth {
|
||||
status: mesh_llm::MeshHealthStatus::Ok,
|
||||
reason: None,
|
||||
},
|
||||
api_base_url: Some("http://127.0.0.1:9337/v1".to_string()),
|
||||
console_url: None,
|
||||
model_id: None,
|
||||
model_name: None,
|
||||
invite_token: None,
|
||||
endpoint_id: None,
|
||||
device_id: None,
|
||||
device_name: None,
|
||||
};
|
||||
let value = serde_json::to_value(&status).expect("serialize mesh status");
|
||||
assert_eq!(value["state"], serde_json::json!("running"));
|
||||
assert_eq!(value["mode"], serde_json::json!("client"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cold_client_preflight_requires_explicit_target() {
|
||||
let state = build_app_state();
|
||||
|
||||
@@ -799,6 +799,7 @@ pub fn run() {
|
||||
mesh_start_node,
|
||||
mesh_stop_node,
|
||||
mesh_node_status,
|
||||
mesh_serving_usage,
|
||||
mesh_installed_models,
|
||||
mesh_model_catalog,
|
||||
update_managed_agent,
|
||||
|
||||
@@ -196,6 +196,80 @@ pub struct MeshNodeStatus {
|
||||
pub device_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Host-side "who is using the compute I'm sharing" snapshot.
|
||||
///
|
||||
/// Read-only projection of the serving node's own runtime metrics (the same
|
||||
/// `routing_metrics` / `inflight_requests` the SDK already exposes on the local
|
||||
/// console). No new trust surface: it reads the node's own status payload.
|
||||
///
|
||||
/// The local/remote/endpoint attempt split is what distinguishes *my own*
|
||||
/// agent (local) from *another member consuming my compute* (remote/endpoint).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MeshServingUsage {
|
||||
/// Requests being served right now.
|
||||
pub inflight: u64,
|
||||
/// Highest concurrent in-flight seen this session.
|
||||
pub peak_inflight: u64,
|
||||
/// Total requests routed through this node.
|
||||
pub requests_served: u64,
|
||||
/// Completion tokens produced.
|
||||
pub tokens_served: u64,
|
||||
/// Recent decode throughput.
|
||||
pub tokens_per_second: f64,
|
||||
/// Requests served for this machine's own agents.
|
||||
pub local_attempts: u64,
|
||||
/// Requests served for a remote peer (someone else consuming my compute).
|
||||
pub remote_attempts: u64,
|
||||
/// Requests served via an advertised endpoint (also a remote consumer).
|
||||
pub endpoint_attempts: u64,
|
||||
/// Other nodes currently visible as peers.
|
||||
pub peers: u64,
|
||||
}
|
||||
|
||||
impl MeshServingUsage {
|
||||
/// True when at least one request has been served for a non-local consumer.
|
||||
pub fn has_remote_consumers(&self) -> bool {
|
||||
self.remote_attempts > 0 || self.endpoint_attempts > 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure extractor: project a raw SDK status payload into [`MeshServingUsage`].
|
||||
///
|
||||
/// Every field is read defensively (missing → 0) so an SDK shape change
|
||||
/// degrades to "no usage shown" rather than an error. Kept pure so it can be
|
||||
/// unit-tested against a captured payload without a live runtime.
|
||||
pub fn serving_usage_from_payload(payload: &serde_json::Value) -> MeshServingUsage {
|
||||
let u64_at = |v: &serde_json::Value| v.as_u64().unwrap_or(0);
|
||||
let rm = payload.get("routing_metrics");
|
||||
let local = rm.and_then(|m| m.get("local_node"));
|
||||
let get_u64 = |obj: Option<&serde_json::Value>, key: &str| {
|
||||
obj.and_then(|o| o.get(key)).map(u64_at).unwrap_or(0)
|
||||
};
|
||||
MeshServingUsage {
|
||||
inflight: local
|
||||
.and_then(|l| l.get("current_inflight_requests"))
|
||||
.map(u64_at)
|
||||
.or_else(|| payload.get("inflight_requests").map(u64_at))
|
||||
.unwrap_or(0),
|
||||
peak_inflight: get_u64(local, "peak_inflight_requests"),
|
||||
requests_served: get_u64(rm, "request_count"),
|
||||
tokens_served: get_u64(rm, "completion_tokens_observed"),
|
||||
tokens_per_second: rm
|
||||
.and_then(|m| m.get("avg_tokens_per_second"))
|
||||
.and_then(serde_json::Value::as_f64)
|
||||
.unwrap_or(0.0),
|
||||
local_attempts: get_u64(local, "local_attempt_count"),
|
||||
remote_attempts: get_u64(local, "remote_attempt_count"),
|
||||
endpoint_attempts: get_u64(local, "endpoint_attempt_count"),
|
||||
peers: payload
|
||||
.get("peers")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|a| a.len() as u64)
|
||||
.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stopped_status() -> MeshNodeStatus {
|
||||
MeshNodeStatus {
|
||||
state: MeshNodeState::Off,
|
||||
@@ -381,6 +455,14 @@ impl DesktopMeshRuntime {
|
||||
&self.start_request
|
||||
}
|
||||
|
||||
/// The role this runtime was started in. Serve = this machine is SHARING
|
||||
/// compute; Client = this machine is CONSUMING a peer's compute. Both
|
||||
/// occupy the single runtime slot, so callers that act only on the sharing
|
||||
/// role (e.g. the Share-compute stop path) must check this first.
|
||||
pub fn mode(&self) -> MeshNodeMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub async fn status(&self) -> anyhow::Result<MeshNodeStatus> {
|
||||
let status = self.handle.status().await?;
|
||||
self.status_from_sdk(status)
|
||||
@@ -420,6 +502,12 @@ impl DesktopMeshRuntime {
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
/// Read-only host-side usage snapshot from the node's own runtime metrics.
|
||||
pub async fn serving_usage(&self) -> anyhow::Result<MeshServingUsage> {
|
||||
let status = self.handle.status().await?;
|
||||
Ok(serving_usage_from_payload(&status.payload))
|
||||
}
|
||||
|
||||
pub async fn dial_endpoint_addr(&self, endpoint_addr: impl Into<String>) -> anyhow::Result<()> {
|
||||
let endpoint_addr = endpoint_addr.into();
|
||||
let validated = validate_advertised_endpoint(&endpoint_addr)?;
|
||||
|
||||
@@ -597,3 +597,69 @@ fn owner_roster_without_membership_list_fails_closed() {
|
||||
|
||||
assert!(super::owner_ids_from_events(&events).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serving_usage_extracts_local_and_remote_attempts() {
|
||||
// Captured shape from a live serving node's status payload. The local vs
|
||||
// remote/endpoint split is what tells "my own agent" apart from "a peer
|
||||
// consuming my compute".
|
||||
let payload = json!({
|
||||
"inflight_requests": 0,
|
||||
"peers": [],
|
||||
"routing_metrics": {
|
||||
"request_count": 5,
|
||||
"completion_tokens_observed": 764,
|
||||
"avg_tokens_per_second": 29.651,
|
||||
"local_node": {
|
||||
"current_inflight_requests": 1,
|
||||
"peak_inflight_requests": 2,
|
||||
"local_attempt_count": 4,
|
||||
"remote_attempt_count": 0,
|
||||
"endpoint_attempt_count": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
let usage = super::serving_usage_from_payload(&payload);
|
||||
assert_eq!(usage.inflight, 1);
|
||||
assert_eq!(usage.peak_inflight, 2);
|
||||
assert_eq!(usage.requests_served, 5);
|
||||
assert_eq!(usage.tokens_served, 764);
|
||||
assert_eq!(usage.local_attempts, 4);
|
||||
assert_eq!(usage.remote_attempts, 0);
|
||||
assert_eq!(usage.endpoint_attempts, 0);
|
||||
assert!(
|
||||
!usage.has_remote_consumers(),
|
||||
"all-local traffic is not a remote consumer"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serving_usage_flags_remote_consumer() {
|
||||
let payload = json!({
|
||||
"peers": [{"id": "a"}, {"id": "b"}],
|
||||
"routing_metrics": {
|
||||
"request_count": 10,
|
||||
"local_node": {
|
||||
"local_attempt_count": 3,
|
||||
"remote_attempt_count": 6,
|
||||
"endpoint_attempt_count": 1
|
||||
}
|
||||
}
|
||||
});
|
||||
let usage = super::serving_usage_from_payload(&payload);
|
||||
assert_eq!(usage.remote_attempts, 6);
|
||||
assert_eq!(usage.endpoint_attempts, 1);
|
||||
assert_eq!(usage.peers, 2);
|
||||
assert!(
|
||||
usage.has_remote_consumers(),
|
||||
"remote/endpoint attempts mean someone else is using my compute"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serving_usage_defaults_to_zero_on_missing_fields() {
|
||||
// SDK shape drift must degrade to "no usage" not panic.
|
||||
let usage = super::serving_usage_from_payload(&json!({}));
|
||||
assert_eq!(usage, super::MeshServingUsage::default());
|
||||
assert!(!usage.has_remote_consumers());
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ pub async fn mesh_node_status(_state: State<'_, AppState>) -> CmdResult<serde_js
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_serving_usage(_state: State<'_, AppState>) -> CmdResult<serde_json::Value> {
|
||||
Err("mesh-llm feature not enabled".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mesh_installed_models(
|
||||
_state: State<'_, AppState>,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { meshServingUsage } from "@/shared/api/tauriMesh";
|
||||
import type { MeshServingUsage } from "@/shared/api/tauriMesh";
|
||||
|
||||
/**
|
||||
* Polls host-side serving usage while this machine is sharing compute.
|
||||
*
|
||||
* Only polls when `enabled` (the card passes `isSharing`) so a machine that
|
||||
* isn't serving does no work. Cadence is a plain 4s — usage is informational,
|
||||
* not a lifecycle transition, so it doesn't need the adaptive stepping that
|
||||
* `useMeshNodeStatus` uses. Returns `null` until the first successful fetch.
|
||||
*/
|
||||
export function useMeshServingUsage(enabled: boolean): MeshServingUsage | null {
|
||||
const [usage, setUsage] = React.useState<MeshServingUsage | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
setUsage(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const fetchOnce = () => {
|
||||
(async () => {
|
||||
try {
|
||||
const value = await meshServingUsage();
|
||||
if (!cancelled) setUsage(value);
|
||||
} catch {
|
||||
// Usage is best-effort; a failed poll leaves the last value in place
|
||||
// rather than flapping the indicator.
|
||||
}
|
||||
})();
|
||||
};
|
||||
fetchOnce();
|
||||
const handle = window.setInterval(fetchOnce, 4000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(handle);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return usage;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { deriveServingIndicator } from "./servingUsage.ts";
|
||||
|
||||
function usage(overrides = {}) {
|
||||
return {
|
||||
inflight: 0,
|
||||
peakInflight: 0,
|
||||
requestsServed: 0,
|
||||
tokensServed: 0,
|
||||
tokensPerSecond: 0,
|
||||
localAttempts: 0,
|
||||
remoteAttempts: 0,
|
||||
endpointAttempts: 0,
|
||||
peers: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("hidden when not sharing", () => {
|
||||
const i = deriveServingIndicator(usage({ requestsServed: 5 }), false);
|
||||
assert.equal(i.show, false);
|
||||
});
|
||||
|
||||
test("hidden when usage not yet fetched", () => {
|
||||
const i = deriveServingIndicator(null, true);
|
||||
assert.equal(i.show, false);
|
||||
});
|
||||
|
||||
test("sharing but nothing served yet -> idle, no detail", () => {
|
||||
const i = deriveServingIndicator(usage(), true);
|
||||
assert.equal(i.show, true);
|
||||
assert.equal(i.active, false);
|
||||
assert.equal(i.hasRemoteConsumers, false);
|
||||
assert.match(i.label, /Idle/);
|
||||
});
|
||||
|
||||
test("only local agent traffic -> not a remote consumer", () => {
|
||||
const i = deriveServingIndicator(
|
||||
usage({ requestsServed: 4, localAttempts: 4, tokensPerSecond: 30 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(i.hasRemoteConsumers, false);
|
||||
assert.match(i.label, /Idle/); // served earlier, none live now
|
||||
assert.match(i.detail, /4 requests served/);
|
||||
});
|
||||
|
||||
test("local agent live now -> serving your agent", () => {
|
||||
const i = deriveServingIndicator(
|
||||
usage({ inflight: 1, localAttempts: 2, tokensPerSecond: 28 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(i.active, true);
|
||||
assert.equal(i.hasRemoteConsumers, false);
|
||||
assert.match(i.label, /Serving your agent/);
|
||||
assert.match(i.label, /1 live/);
|
||||
});
|
||||
|
||||
test("remote consumer, not live -> used by another member (headline case)", () => {
|
||||
const i = deriveServingIndicator(
|
||||
usage({
|
||||
requestsServed: 7,
|
||||
remoteAttempts: 6,
|
||||
endpointAttempts: 1,
|
||||
peers: 2,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(i.hasRemoteConsumers, true);
|
||||
assert.equal(i.active, false);
|
||||
assert.match(i.label, /another member/);
|
||||
assert.match(i.label, /7 requests/); // remote+endpoint = 7
|
||||
assert.match(i.detail, /2 peers/);
|
||||
});
|
||||
|
||||
test("remote consumer live now -> in use now, singular peer/request grammar", () => {
|
||||
const i = deriveServingIndicator(
|
||||
usage({ inflight: 1, remoteAttempts: 1, peers: 1, tokensPerSecond: 31 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(i.active, true);
|
||||
assert.equal(i.hasRemoteConsumers, true);
|
||||
assert.match(i.label, /In use now by another member/);
|
||||
assert.match(i.detail, /1 peer\b/); // singular
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { MeshServingUsage } from "@/shared/api/tauriMesh";
|
||||
|
||||
/**
|
||||
* Pure projection of host-side serving usage into a small, politely-worded
|
||||
* indicator model for the Share compute card.
|
||||
*
|
||||
* Single source of truth for "who is using the compute I'm sharing" copy, so
|
||||
* the component and its tests agree. Kept pure/total (accepts null = not yet
|
||||
* fetched) and defensive (all fields optional-safe via the Rust extractor).
|
||||
*
|
||||
* Distinctions that matter:
|
||||
* - `localAttempts` = this machine's OWN agents using the local model. Not a
|
||||
* "someone else is here" signal — surfaced softly as activity, not as a peer.
|
||||
* - `remoteAttempts` / `endpointAttempts` = another member consuming this
|
||||
* machine's compute. THIS is the "someone connected to what I'm sharing"
|
||||
* signal.
|
||||
*/
|
||||
export type MeshServingIndicator = {
|
||||
/** Whether to show anything at all (only while actively sharing). */
|
||||
show: boolean;
|
||||
/** Someone is being served right now. */
|
||||
active: boolean;
|
||||
/** A non-local member is (or has been) consuming this machine's compute. */
|
||||
hasRemoteConsumers: boolean;
|
||||
/** One-line status suitable for the card. */
|
||||
label: string;
|
||||
/** Longer detail for a tooltip / secondary line. */
|
||||
detail: string | null;
|
||||
};
|
||||
|
||||
function plural(n: number, one: string, many = `${one}s`): string {
|
||||
return n === 1 ? one : many;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param usage latest snapshot from `meshServingUsage`, or null if not fetched
|
||||
* @param isSharing whether this machine is currently in serve mode (card owns
|
||||
* this from the toggle model). Usage is only meaningful while
|
||||
* sharing.
|
||||
*/
|
||||
export function deriveServingIndicator(
|
||||
usage: MeshServingUsage | null,
|
||||
isSharing: boolean,
|
||||
): MeshServingIndicator {
|
||||
const hidden: MeshServingIndicator = {
|
||||
show: false,
|
||||
active: false,
|
||||
hasRemoteConsumers: false,
|
||||
label: "",
|
||||
detail: null,
|
||||
};
|
||||
if (!isSharing || !usage) {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
const hasRemoteConsumers =
|
||||
usage.remoteAttempts > 0 || usage.endpointAttempts > 0;
|
||||
const active = usage.inflight > 0;
|
||||
|
||||
// Remote consumer present (or seen) — the headline case the user asked for.
|
||||
if (hasRemoteConsumers) {
|
||||
const remote = usage.remoteAttempts + usage.endpointAttempts;
|
||||
const label = active
|
||||
? `In use now by another member · ${usage.inflight} live`
|
||||
: `Used by another member · ${remote} ${plural(remote, "request")}`;
|
||||
const detail =
|
||||
usage.peers > 0
|
||||
? `${usage.peers} ${plural(usage.peers, "peer")} on the mesh · ${Math.round(usage.tokensPerSecond)} tok/s`
|
||||
: `${Math.round(usage.tokensPerSecond)} tok/s`;
|
||||
return { show: true, active, hasRemoteConsumers: true, label, detail };
|
||||
}
|
||||
|
||||
// Only local (this machine's own agents) — show softly as activity.
|
||||
if (active) {
|
||||
return {
|
||||
show: true,
|
||||
active: true,
|
||||
hasRemoteConsumers: false,
|
||||
label: `Serving your agent · ${usage.inflight} live`,
|
||||
detail: `${Math.round(usage.tokensPerSecond)} tok/s`,
|
||||
};
|
||||
}
|
||||
if (usage.requestsServed > 0) {
|
||||
return {
|
||||
show: true,
|
||||
active: false,
|
||||
hasRemoteConsumers: false,
|
||||
label: "Idle · no one using it right now",
|
||||
detail: `${usage.requestsServed} ${plural(usage.requestsServed, "request")} served this session`,
|
||||
};
|
||||
}
|
||||
|
||||
// Sharing but nothing served yet.
|
||||
return {
|
||||
show: true,
|
||||
active: false,
|
||||
hasRemoteConsumers: false,
|
||||
label: "Idle · no one using it yet",
|
||||
detail: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { deriveMeshShareToggle } from "./shareToggleState.ts";
|
||||
|
||||
const status = (overrides = {}) => ({
|
||||
state: "off",
|
||||
mode: null,
|
||||
health: { status: "ok", reason: null },
|
||||
apiBaseUrl: null,
|
||||
consoleUrl: null,
|
||||
modelId: null,
|
||||
modelName: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("serve-mode running/starting reads as sharing", () => {
|
||||
for (const state of ["running", "starting"]) {
|
||||
const model = deriveMeshShareToggle(status({ state, mode: "serve" }));
|
||||
assert.equal(model.isSharing, true, `serve+${state} should be sharing`);
|
||||
assert.equal(model.isConsuming, false);
|
||||
assert.equal(model.slotOccupied, true);
|
||||
}
|
||||
});
|
||||
|
||||
test("client-mode running/starting is consuming, NOT sharing (regression)", () => {
|
||||
// The core bug: consuming a peer's compute starts a client node in the same
|
||||
// slot, which reports state:"running". The Share toggle must stay off.
|
||||
for (const state of ["running", "starting"]) {
|
||||
const model = deriveMeshShareToggle(status({ state, mode: "client" }));
|
||||
assert.equal(
|
||||
model.isSharing,
|
||||
false,
|
||||
`client+${state} must NOT light the Share toggle`,
|
||||
);
|
||||
assert.equal(model.isConsuming, true);
|
||||
assert.equal(model.slotOccupied, true);
|
||||
}
|
||||
});
|
||||
|
||||
test("a FAILED serve node still occupies the slot and stays turn-off-able", () => {
|
||||
// Expert review: a failed runtime still holds the single slot, so a fresh
|
||||
// start would throw "already running". It must read as sharing (so the
|
||||
// switch stays on and can be turned OFF to clear/retry) and occupy the slot.
|
||||
const model = deriveMeshShareToggle(
|
||||
status({ state: "failed", mode: "serve" }),
|
||||
);
|
||||
assert.equal(
|
||||
model.isSharing,
|
||||
true,
|
||||
"failed serve node is still turn-off-able",
|
||||
);
|
||||
assert.equal(model.slotOccupied, true);
|
||||
});
|
||||
|
||||
test("a FAILED client node occupies the slot but is not sharing", () => {
|
||||
const model = deriveMeshShareToggle(
|
||||
status({ state: "failed", mode: "client" }),
|
||||
);
|
||||
assert.equal(model.isSharing, false);
|
||||
assert.equal(model.isConsuming, true);
|
||||
assert.equal(model.slotOccupied, true);
|
||||
});
|
||||
|
||||
test("off / stopping never occupy the slot or read as sharing/consuming", () => {
|
||||
for (const state of ["off", "stopping"]) {
|
||||
for (const mode of [null, "serve", "client"]) {
|
||||
const model = deriveMeshShareToggle(status({ state, mode }));
|
||||
assert.equal(model.isSharing, false, `${mode}+${state} not sharing`);
|
||||
assert.equal(model.isConsuming, false, `${mode}+${state} not consuming`);
|
||||
assert.equal(model.slotOccupied, false, `${mode}+${state} slot free`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("null status (not yet fetched) is neither sharing nor consuming", () => {
|
||||
const model = deriveMeshShareToggle(null);
|
||||
assert.equal(model.isSharing, false);
|
||||
assert.equal(model.isConsuming, false);
|
||||
assert.equal(model.slotOccupied, false);
|
||||
});
|
||||
|
||||
test("running with a missing mode occupies the slot but is not sharing", () => {
|
||||
// Defensive: a status that somehow lacks mode must not default to "on", but
|
||||
// it DOES hold the slot (a fresh start would fail), so it stays occupied.
|
||||
const model = deriveMeshShareToggle(status({ state: "running", mode: null }));
|
||||
assert.equal(model.isSharing, false);
|
||||
assert.equal(model.isConsuming, false);
|
||||
assert.equal(model.slotOccupied, true);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { MeshNodeStatus } from "@/shared/api/tauriMesh";
|
||||
|
||||
/**
|
||||
* Derived Share-compute toggle model.
|
||||
*
|
||||
* The single mesh runtime slot is shared by BOTH roles: serve mode (this
|
||||
* machine SHARING compute) and client mode (this machine CONSUMING a peer's
|
||||
* compute). Both report `state: "running"`. The Share toggle must therefore
|
||||
* key off `mode`, not `state` alone — otherwise consuming a peer's compute
|
||||
* lights up the Share switch and (worse) clicking it tears down the unrelated
|
||||
* client session. See `deriveMeshShareToggle`.
|
||||
*/
|
||||
export type MeshShareToggleModel = {
|
||||
/**
|
||||
* The Share switch is on: a serve-mode node occupies the slot. Stays true
|
||||
* while it is starting or even if it later fails health (the runtime still
|
||||
* occupies the slot, and the user must be able to turn it off to clear/retry
|
||||
* — see `StatusLine` for the health sub-state). A serve node that also routes
|
||||
* a peer's model is still sharing: routing is a capability, not a role.
|
||||
*/
|
||||
isSharing: boolean;
|
||||
/**
|
||||
* A client-mode runtime occupies the single slot (this machine is consuming
|
||||
* a peer's compute). The Share switch must read off + disabled while true.
|
||||
*/
|
||||
isConsuming: boolean;
|
||||
/**
|
||||
* ANY runtime occupies the single slot (serve or client, healthy or failed).
|
||||
* A fresh `mesh_start_node` fails with "already running" while true, so the
|
||||
* switch must not offer a start — only a stop of an existing serve node.
|
||||
*/
|
||||
slotOccupied: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A runtime object occupies the slot once it is starting or running — and also
|
||||
* when it has `failed` (it started, then errored; the runtime is still in the
|
||||
* slot and blocks a fresh start). `off`/`stopping` do not occupy it.
|
||||
*/
|
||||
function occupiesSlot(status: MeshNodeStatus | null): boolean {
|
||||
return (
|
||||
status?.state === "running" ||
|
||||
status?.state === "starting" ||
|
||||
status?.state === "failed"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a mesh node status into the Share toggle's on/consuming state.
|
||||
*
|
||||
* Pure and total (accepts `null` = status not yet fetched). This is the single
|
||||
* source of truth for "is this machine sharing?" — the component and its
|
||||
* regression tests both consume it so the `state`-only bug cannot come back.
|
||||
*/
|
||||
export function deriveMeshShareToggle(
|
||||
status: MeshNodeStatus | null,
|
||||
): MeshShareToggleModel {
|
||||
const occupied = occupiesSlot(status);
|
||||
return {
|
||||
isSharing: occupied && status?.mode === "serve",
|
||||
isConsuming: occupied && status?.mode === "client",
|
||||
slotOccupied: occupied,
|
||||
};
|
||||
}
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
useMeshDownloadProgress,
|
||||
} from "../hooks/useMeshDownloadProgress";
|
||||
import { useMeshNodeStatus } from "../hooks/useMeshNodeStatus";
|
||||
import { useMeshServingUsage } from "../hooks/useMeshServingUsage";
|
||||
import { deriveMeshShareToggle } from "../shareToggleState";
|
||||
import { deriveServingIndicator } from "../servingUsage";
|
||||
|
||||
const MODEL_DRAFT_STORAGE_KEY = "buzz.mesh-compute.share.model.v1";
|
||||
const MAX_VRAM_DRAFT_STORAGE_KEY = "buzz.mesh-compute.share.max-vram-gb.v1";
|
||||
@@ -122,16 +125,35 @@ export function MeshComputeSettingsCard() {
|
||||
}, []);
|
||||
|
||||
// Mirror the running node's modelId back into the field so the card shows
|
||||
// what's actually being served, even after a fresh app load.
|
||||
// what's ACTUALLY being served — not just on a fresh load (empty field) but
|
||||
// whenever the served model differs from the field (e.g. the member switched
|
||||
// models and the node restarted on the new one). This is safe from clobbering
|
||||
// a mid-edit because the field is disabled whenever the slot is occupied
|
||||
// (`controlsDisabled`), so the user can't be typing while a node is running.
|
||||
React.useEffect(() => {
|
||||
if (status?.state === "running" && status.modelId && modelInput === "") {
|
||||
if (
|
||||
status?.state === "running" &&
|
||||
status.modelId &&
|
||||
status.modelId !== modelInput
|
||||
) {
|
||||
setModelInput(status.modelId);
|
||||
writeDraft(MODEL_DRAFT_STORAGE_KEY, status.modelId);
|
||||
}
|
||||
}, [status?.state, status?.modelId, modelInput]);
|
||||
|
||||
const isOn = status?.state === "running" || status?.state === "starting";
|
||||
const controlsDisabled = isOn || actionInFlight;
|
||||
// The Share toggle reflects ONLY serve-mode occupancy. A client-mode runtime
|
||||
// (this machine consuming a peer's compute) shares the single runtime slot
|
||||
// and also reports state:"running" — but it must NOT light this switch, and
|
||||
// toggling off must never tear down that unrelated consume session.
|
||||
const { isSharing, isConsuming, slotOccupied } =
|
||||
deriveMeshShareToggle(status);
|
||||
// Host-side "who is using the compute I'm sharing" — only polled while
|
||||
// actively sharing (serve mode). Read-only; reads the node's own metrics.
|
||||
const servingUsage = useMeshServingUsage(isSharing);
|
||||
const servingIndicator = deriveServingIndicator(servingUsage, isSharing);
|
||||
// Any occupying runtime (serve or client, healthy or failed) locks the model
|
||||
// inputs and blocks a fresh start — stop it before reconfiguring.
|
||||
const controlsDisabled = slotOccupied || actionInFlight;
|
||||
const refClass = classifyModelRef(modelInput);
|
||||
const canStart =
|
||||
refClass.kind !== "unknown" &&
|
||||
@@ -139,6 +161,13 @@ export function MeshComputeSettingsCard() {
|
||||
status?.state !== "starting";
|
||||
|
||||
async function handleToggle(next: boolean) {
|
||||
// Never let the Share switch tear down a consume session. The switch is
|
||||
// already disabled while consuming, but status can be stale between polls,
|
||||
// so refuse a stop that isn't stopping OUR serve node as a belt-and-braces
|
||||
// guard (the backend enforces this authoritatively too).
|
||||
if (!next && !isSharing) {
|
||||
return;
|
||||
}
|
||||
setActionError(null);
|
||||
setPendingAction(next ? "start" : "stop");
|
||||
setActionInFlight(true);
|
||||
@@ -202,12 +231,43 @@ export function MeshComputeSettingsCard() {
|
||||
>
|
||||
Share this machine
|
||||
</label>
|
||||
<StatusLine pendingAction={pendingAction} status={status} />
|
||||
<StatusLine
|
||||
isConsuming={isConsuming}
|
||||
pendingAction={pendingAction}
|
||||
status={status}
|
||||
/>
|
||||
{servingIndicator.show ? (
|
||||
<p
|
||||
className={
|
||||
servingIndicator.hasRemoteConsumers
|
||||
? "mt-0.5 text-2xs text-emerald-600 dark:text-emerald-400"
|
||||
: "mt-0.5 text-2xs text-muted-foreground"
|
||||
}
|
||||
data-testid="mesh-serving-usage"
|
||||
title={servingIndicator.detail ?? undefined}
|
||||
>
|
||||
{servingIndicator.label}
|
||||
{servingIndicator.detail ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
· {servingIndicator.detail}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch
|
||||
checked={isOn}
|
||||
checked={isSharing}
|
||||
data-testid="mesh-share-compute-toggle"
|
||||
disabled={actionInFlight || (!isOn && !canStart)}
|
||||
disabled={
|
||||
// When the slot is occupied, the switch is only actionable if
|
||||
// WE are sharing (so it can stop a serve node — even a failed
|
||||
// one). Any other occupant (consuming, or an unexpected
|
||||
// modeless-running node) would make a fresh start throw "already
|
||||
// running", so keep it disabled. When the slot is empty, gate on
|
||||
// a valid model ref.
|
||||
actionInFlight || (slotOccupied ? !isSharing : !canStart)
|
||||
}
|
||||
id="mesh-share-compute-toggle"
|
||||
onCheckedChange={handleToggle}
|
||||
/>
|
||||
@@ -488,9 +548,11 @@ function CatalogPicker({
|
||||
}
|
||||
|
||||
function StatusLine({
|
||||
isConsuming,
|
||||
pendingAction,
|
||||
status,
|
||||
}: {
|
||||
isConsuming: boolean;
|
||||
pendingAction: "start" | "stop" | null;
|
||||
status: MeshNodeStatus | null;
|
||||
}) {
|
||||
@@ -500,6 +562,18 @@ function StatusLine({
|
||||
if (pendingAction === "stop") {
|
||||
return <p className="text-sm text-muted-foreground">Stopping…</p>;
|
||||
}
|
||||
// A client-mode runtime owns the single slot: this machine is consuming a
|
||||
// peer's compute, not sharing. Explain why Share is off + disabled instead
|
||||
// of showing the misleading "Sharing … with relay members" serve copy. The
|
||||
// client node is app-session-lived infra with no user stop control, so the
|
||||
// copy states the fact rather than promising an action that doesn't exist.
|
||||
if (isConsuming) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This machine is currently using another member's shared compute.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (!status) {
|
||||
return <p className="text-sm text-muted-foreground">Checking status…</p>;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,27 @@ export async function meshNodeStatus(): Promise<MeshNodeStatus> {
|
||||
return await invokeTauri<MeshNodeStatus>("mesh_node_status");
|
||||
}
|
||||
|
||||
/**
|
||||
* Host-side usage of the compute this machine is sharing. The
|
||||
* local/remote/endpoint attempt split distinguishes this machine's own agents
|
||||
* (local) from another member consuming this machine's compute (remote/endpoint).
|
||||
*/
|
||||
export type MeshServingUsage = {
|
||||
inflight: number;
|
||||
peakInflight: number;
|
||||
requestsServed: number;
|
||||
tokensServed: number;
|
||||
tokensPerSecond: number;
|
||||
localAttempts: number;
|
||||
remoteAttempts: number;
|
||||
endpointAttempts: number;
|
||||
peers: number;
|
||||
};
|
||||
|
||||
export async function meshServingUsage(): Promise<MeshServingUsage> {
|
||||
return await invokeTauri<MeshServingUsage>("mesh_serving_usage");
|
||||
}
|
||||
|
||||
export async function meshInstalledModels(): Promise<MeshModelOption[]> {
|
||||
return await invokeTauri<MeshModelOption[]>("mesh_installed_models");
|
||||
}
|
||||
|
||||
@@ -1049,6 +1049,26 @@ declare global {
|
||||
admitted?: boolean;
|
||||
models?: Array<{ id: string; name: string | null }>;
|
||||
denyReason?: string;
|
||||
/** Seed the runtime slot's lifecycle state (default "off"). */
|
||||
nodeState?: "off" | "running";
|
||||
/**
|
||||
* Seed the runtime slot's role. "client" models this machine CONSUMING a
|
||||
* peer's compute — it shares the single slot and reports state:"running",
|
||||
* so the Share toggle must stay off. Drives the toggle-on regression test.
|
||||
*/
|
||||
nodeMode?: "serve" | "client" | null;
|
||||
/** Seed host-side serving usage to exercise the "who's using my compute" indicator. */
|
||||
servingUsage?: Partial<{
|
||||
inflight: number;
|
||||
peakInflight: number;
|
||||
requestsServed: number;
|
||||
tokensServed: number;
|
||||
tokensPerSecond: number;
|
||||
localAttempts: number;
|
||||
remoteAttempts: number;
|
||||
endpointAttempts: number;
|
||||
peers: number;
|
||||
}>;
|
||||
}) => void;
|
||||
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
|
||||
agentPubkey: string;
|
||||
@@ -2762,12 +2782,37 @@ function resetMockSaveSubscriptions(config: E2eConfig | undefined) {
|
||||
// in a browser. They deliberately do NOT model real admission, real inference,
|
||||
// or real mesh routing — those are proven by the Rust layer-2 tests and the
|
||||
// on-hardware layer-1 example. Do not port any of this into production code.
|
||||
type MockServingUsage = {
|
||||
inflight: number;
|
||||
peakInflight: number;
|
||||
requestsServed: number;
|
||||
tokensServed: number;
|
||||
tokensPerSecond: number;
|
||||
localAttempts: number;
|
||||
remoteAttempts: number;
|
||||
endpointAttempts: number;
|
||||
peers: number;
|
||||
};
|
||||
|
||||
const ZERO_SERVING_USAGE: MockServingUsage = {
|
||||
inflight: 0,
|
||||
peakInflight: 0,
|
||||
requestsServed: 0,
|
||||
tokensServed: 0,
|
||||
tokensPerSecond: 0,
|
||||
localAttempts: 0,
|
||||
remoteAttempts: 0,
|
||||
endpointAttempts: 0,
|
||||
peers: 0,
|
||||
};
|
||||
|
||||
const mockMeshState: {
|
||||
admitted: boolean;
|
||||
models: Array<{ id: string; name: string | null }>;
|
||||
denyReason: string;
|
||||
nodeState: "off" | "running";
|
||||
nodeMode: "serve" | "client" | null;
|
||||
servingUsage: MockServingUsage;
|
||||
} = {
|
||||
admitted: true,
|
||||
models: [
|
||||
@@ -2776,6 +2821,7 @@ const mockMeshState: {
|
||||
denyReason: "not a relay member",
|
||||
nodeState: "off",
|
||||
nodeMode: null,
|
||||
servingUsage: { ...ZERO_SERVING_USAGE },
|
||||
};
|
||||
|
||||
function resetMockMesh() {
|
||||
@@ -2786,6 +2832,7 @@ function resetMockMesh() {
|
||||
mockMeshState.denyReason = "not a relay member";
|
||||
mockMeshState.nodeState = "off";
|
||||
mockMeshState.nodeMode = null;
|
||||
mockMeshState.servingUsage = { ...ZERO_SERVING_USAGE };
|
||||
}
|
||||
let mockPersonas: RawPersona[] = [];
|
||||
let mockTeams: RawTeam[] = [];
|
||||
@@ -9110,6 +9157,13 @@ export function maybeInstallE2eTauriMocks() {
|
||||
if (mesh.models !== undefined) mockMeshState.models = mesh.models;
|
||||
if (mesh.denyReason !== undefined)
|
||||
mockMeshState.denyReason = mesh.denyReason;
|
||||
if (mesh.nodeState !== undefined) mockMeshState.nodeState = mesh.nodeState;
|
||||
if (mesh.nodeMode !== undefined) mockMeshState.nodeMode = mesh.nodeMode;
|
||||
if (mesh.servingUsage !== undefined)
|
||||
mockMeshState.servingUsage = {
|
||||
...mockMeshState.servingUsage,
|
||||
...mesh.servingUsage,
|
||||
};
|
||||
};
|
||||
let seedTurnSeq = Date.now();
|
||||
window.__BUZZ_E2E_SEED_ACTIVE_TURNS__ = ({
|
||||
@@ -9229,6 +9283,8 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return mockMeshState.models;
|
||||
case "mesh_node_status":
|
||||
return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
|
||||
case "mesh_serving_usage":
|
||||
return mockMeshState.servingUsage;
|
||||
case "mesh_start_node": {
|
||||
const req = (
|
||||
payload as { request?: { mode?: "serve" | "client" } } | null
|
||||
@@ -9238,6 +9294,14 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
|
||||
}
|
||||
case "mesh_stop_node":
|
||||
// Mirror the backend contract: "stop sharing" must never tear down a
|
||||
// client (consume) node occupying the single slot. Leave it running.
|
||||
if (mockMeshState.nodeMode === "client") {
|
||||
return meshNodeStatus(
|
||||
mockMeshState.nodeState,
|
||||
mockMeshState.nodeMode,
|
||||
);
|
||||
}
|
||||
mockMeshState.nodeState = "off";
|
||||
mockMeshState.nodeMode = null;
|
||||
return meshNodeStatus("off", null);
|
||||
|
||||
@@ -5,6 +5,10 @@ import { openSettings } from "../helpers/settings";
|
||||
|
||||
type E2eWindow = Window & {
|
||||
__BUZZ_E2E_COMMANDS__?: string[];
|
||||
__BUZZ_E2E_SET_MESH__?: (mesh: {
|
||||
nodeState?: "off" | "running";
|
||||
nodeMode?: "serve" | "client" | null;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -51,3 +55,44 @@ test("Share compute has a clear empty state and starts and stops sharing", async
|
||||
)
|
||||
.toContain("mesh_stop_node");
|
||||
});
|
||||
|
||||
test("consuming a peer's compute does NOT light the Share toggle", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Regression: consuming someone else's shared compute starts a client-mode
|
||||
// node in the single runtime slot, which reports state:"running". The Share
|
||||
// toggle keyed off state alone and lit up — and clicking it would have torn
|
||||
// down the unrelated consume session. It must stay off + disabled, explain
|
||||
// why, and issue no stop command.
|
||||
await page.goto("/");
|
||||
// The mesh seed hook is installed when the mock bridge boots; calling it
|
||||
// before then silently no-ops (optional chaining) and the seed is lost.
|
||||
await page.waitForFunction(
|
||||
() => typeof (window as E2eWindow).__BUZZ_E2E_SET_MESH__ === "function",
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
(window as E2eWindow).__BUZZ_E2E_SET_MESH__?.({
|
||||
nodeState: "running",
|
||||
nodeMode: "client",
|
||||
});
|
||||
});
|
||||
await openSettings(page, "compute");
|
||||
|
||||
const card = page.getByTestId("settings-mesh-share-compute");
|
||||
const toggle = page.getByTestId("mesh-share-compute-toggle");
|
||||
|
||||
await expect(card).toContainText(
|
||||
"This machine is currently using another member's shared compute",
|
||||
);
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await expect(toggle).toBeDisabled();
|
||||
|
||||
// The switch is disabled, so a click can't fire onCheckedChange — but assert
|
||||
// the destructive command never went out regardless.
|
||||
const stopIssued = await page.evaluate(() =>
|
||||
((window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []).includes(
|
||||
"mesh_stop_node",
|
||||
),
|
||||
);
|
||||
expect(stopIssued).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user