diff --git a/Cargo.lock b/Cargo.lock
index 3b60dc4579..40f33e13b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -43,6 +43,19 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if 1.0.4",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.4"
@@ -2174,7 +2187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
 dependencies = [
  "data-encoding",
- "syn 1.0.109",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -3006,8 +3019,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]]
@@ -3606,7 +3619,7 @@ dependencies = [
  "js-sys",
  "log",
  "wasm-bindgen",
- "windows-core 0.62.2",
+ "windows-core 0.61.2",
 ]
 
 [[package]]
@@ -4498,8 +4511,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "hex",
  "mesh-llm-client",
@@ -4508,8 +4521,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-api-client",
@@ -4519,13 +4532,13 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 
 [[package]]
 name = "mesh-llm-client"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -4556,8 +4569,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -4572,8 +4585,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-host-runtime",
@@ -4582,8 +4595,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "clap",
@@ -4594,8 +4607,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "cc",
@@ -4607,8 +4620,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "serde",
  "serde_json",
@@ -4616,16 +4629,16 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "mesh-llm-native-runtime",
 ]
 
 [[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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "argon2",
@@ -4647,7 +4660,6 @@ dependencies = [
  "http",
  "http-body-util",
  "httparse",
- "if-addrs",
  "iroh",
  "json5",
  "keyring",
@@ -4695,6 +4707,7 @@ dependencies = [
  "serde_yaml",
  "sha2 0.10.9",
  "skippy-coordinator",
+ "skippy-ffi",
  "skippy-protocol",
  "skippy-runtime",
  "skippy-server",
@@ -4717,8 +4730,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "argon2",
  "base64",
@@ -4739,8 +4752,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "serde",
@@ -4750,8 +4763,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-types",
@@ -4764,8 +4777,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -4781,8 +4794,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -4792,6 +4805,7 @@ dependencies = [
  "reqwest 0.12.28",
  "serde",
  "serde_json",
+ "sha2 0.10.9",
  "tar",
  "tempfile",
  "zip",
@@ -4799,8 +4813,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "hex",
@@ -4812,16 +4826,16 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "iroh",
 ]
 
 [[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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -4843,8 +4857,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-api-client",
@@ -4858,8 +4872,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -4869,8 +4883,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "chrono",
@@ -4892,8 +4906,8 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "hex",
  "serde",
@@ -4903,13 +4917,13 @@ 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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 
 [[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"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "async-trait",
  "mesh-llm-guardrails",
@@ -5035,8 +5049,8 @@ dependencies = [
 
 [[package]]
 name = "model-artifact"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5046,8 +5060,8 @@ dependencies = [
 
 [[package]]
 name = "model-hf"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5064,8 +5078,8 @@ dependencies = [
 
 [[package]]
 name = "model-package"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "bytes",
@@ -5084,16 +5098,16 @@ dependencies = [
 
 [[package]]
 name = "model-ref"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "model-resolver"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "model-artifact",
@@ -5812,8 +5826,8 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
 [[package]]
 name = "openai-frontend"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "async-trait",
  "axum",
@@ -7365,9 +7379,9 @@ dependencies = [
 
 [[package]]
 name = "rmcp"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e"
+checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59"
 dependencies = [
  "async-trait",
  "base64",
@@ -7398,9 +7412,9 @@ dependencies = [
 
 [[package]]
 name = "rmcp-macros"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d"
+checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5"
 dependencies = [
  "darling 0.23.0",
  "proc-macro2",
@@ -8150,8 +8164,8 @@ checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
 
 [[package]]
 name = "skippy-cache"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "blake3",
@@ -8160,29 +8174,29 @@ dependencies = [
 
 [[package]]
 name = "skippy-coordinator"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "thiserror 2.0.18",
 ]
 
 [[package]]
 name = "skippy-ffi"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "libloading",
 ]
 
 [[package]]
 name = "skippy-metrics"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 
 [[package]]
 name = "skippy-protocol"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "prost",
  "prost-build",
@@ -8192,8 +8206,8 @@ dependencies = [
 
 [[package]]
 name = "skippy-runtime"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "libc",
@@ -8206,9 +8220,10 @@ dependencies = [
 
 [[package]]
 name = "skippy-server"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
+ "ahash",
  "anyhow",
  "async-trait",
  "axum",
@@ -8234,8 +8249,8 @@ dependencies = [
 
 [[package]]
 name = "skippy-topology"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.73.1#43103c5c40292be688ac0261129bcbab0e7b9132"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "serde",
  "serde_json",
@@ -10462,8 +10477,8 @@ dependencies = [
  "log",
  "serde",
  "thiserror 2.0.18",
- "windows 0.62.2",
- "windows-core 0.62.2",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
 ]
 
 [[package]]
diff --git a/crates/buzz-acp/src/acp.rs b/crates/buzz-acp/src/acp.rs
index 8a698954a0..b1626d3f75 100644
--- a/crates/buzz-acp/src/acp.rs
+++ b/crates/buzz-acp/src/acp.rs
@@ -211,6 +211,56 @@ pub struct AcpClient {
     /// deltas. Both goose and buzz-agent emit this notification; goose gates
     /// on client capability advertisement, buzz-agent emits unconditionally.
     goose_usage: UsageTracker,
+    /// Plain assistant text streamed during the current turn, and whether the
+    /// agent published a reply itself. Small local models routinely finish a
+    /// multi-step turn by writing the answer as prose instead of calling
+    /// `send_message`; ACP treats streamed text as observability only, so that
+    /// answer is dropped. The pool's mesh-gated delivery fallback consumes
+    /// these via [`take_undelivered_turn_text`](Self::take_undelivered_turn_text).
+    turn_text: String,
+    /// Whether a message-publish tool call was observed during this turn.
+    turn_published: bool,
+}
+
+/// Maximum assistant text retained per turn for the delivery fallback.
+///
+/// Bounded so a rogue agent streaming forever cannot grow the buffer without
+/// limit; a chat reply that exceeds this is truncated rather than dropped.
+const MAX_TURN_TEXT_BYTES: usize = 8 * 1024;
+
+/// Whether an ACP `tool_call` update represents the agent publishing a message.
+///
+/// Matches both shapes an agent can use: the first-class `send_message` tool
+/// (any `dev__`/server prefix) and a shell command that runs the CLI directly.
+fn is_message_publish(title: &str, raw_input: &str) -> bool {
+    title.ends_with("send_message")
+        || raw_input.contains("messages send")
+        || raw_input.contains("social publish")
+}
+
+/// Whether text is a bare acknowledgement not worth publishing as a reply.
+///
+/// Guards the fallback against posting filler like "OK" or "Done." when the
+/// agent had nothing substantive to say.
+fn is_bare_acknowledgement(text: &str) -> bool {
+    let normalized: String = text
+        .chars()
+        .filter(|c| c.is_alphanumeric() || c.is_whitespace())
+        .collect::<String>()
+        .trim()
+        .to_ascii_lowercase();
+    matches!(
+        normalized.as_str(),
+        "" | "ok"
+            | "okay"
+            | "done"
+            | "sure"
+            | "got it"
+            | "understood"
+            | "acknowledged"
+            | "will do"
+            | "thanks"
+    )
 }
 
 /// Recursively merge `overlay` into `base`, with `overlay` winning on scalar/shape
@@ -550,6 +600,8 @@ impl AcpClient {
             steering_supported: false,
             steer_rx: None,
             goose_usage: UsageTracker::default(),
+            turn_text: String::new(),
+            turn_published: false,
         })
     }
 
@@ -760,6 +812,11 @@ impl AcpClient {
         // misattributed to this turn.
         self.goose_usage.begin_turn(session_id);
 
+        // Reset per-turn delivery-fallback state alongside usage: text streamed
+        // by a previous turn must never be republished by this one.
+        self.turn_text.clear();
+        self.turn_published = false;
+
         self.last_prompt_id = Some(self.next_id);
         let id = self.next_id;
         self.next_id += 1;
@@ -864,6 +921,23 @@ impl AcpClient {
         self.goose_usage.take()
     }
 
+    /// Take the turn's assistant text if the agent never published a reply.
+    ///
+    /// Returns `None` when the agent published via `send_message` (or the CLI)
+    /// itself, or when the only text was a bare acknowledgement. Consuming
+    /// clears the buffer so the same text can never be posted twice.
+    pub fn take_undelivered_turn_text(&mut self) -> Option<String> {
+        let text = std::mem::take(&mut self.turn_text);
+        if self.turn_published {
+            return None;
+        }
+        let trimmed = text.trim();
+        if trimmed.is_empty() || is_bare_acknowledgement(trimmed) {
+            return None;
+        }
+        Some(trimmed.to_string())
+    }
+
     /// Install a per-turn steer request channel for goose-native
     /// non-cancelling mid-turn delivery.
     ///
@@ -1715,6 +1789,15 @@ impl AcpClient {
             "agent_message_chunk" => {
                 if let Some(text) = update["content"]["text"].as_str() {
                     tracing::info!(target: "acp::stream", "{text}");
+                    // Retain for the mesh-gated delivery fallback (bounded).
+                    let remaining = MAX_TURN_TEXT_BYTES.saturating_sub(self.turn_text.len());
+                    if remaining > 0 {
+                        let mut end = text.len().min(remaining);
+                        while end > 0 && !text.is_char_boundary(end) {
+                            end -= 1;
+                        }
+                        self.turn_text.push_str(&text[..end]);
+                    }
                 }
                 false
             }
@@ -1728,6 +1811,15 @@ impl AcpClient {
                     .and_then(|v| v.as_str())
                     .unwrap_or("unknown");
                 tracing::info!(target: "acp::tool", "tool_call: {title} ({kind})");
+                if !self.turn_published {
+                    let raw_input = update
+                        .get("rawInput")
+                        .map(|v| v.to_string())
+                        .unwrap_or_default();
+                    if is_message_publish(title, &raw_input) {
+                        self.turn_published = true;
+                    }
+                }
                 true
             }
             "tool_call_update" => {
@@ -2223,6 +2315,43 @@ fn configure_no_window(cmd: &mut tokio::process::Command) {
 mod tests {
     use super::*;
 
+    #[test]
+    fn publish_detection_matches_tool_and_shell_shapes() {
+        // First-class tool, with the MCP server prefix the agent reports.
+        assert!(is_message_publish("dev__send_message", "{}"));
+        assert!(is_message_publish("send_message", "{}"));
+        // Shell shapes: the CLI invoked directly.
+        assert!(is_message_publish(
+            "dev__shell",
+            r#"{"command":"buzz messages send --channel c --content hi"}"#
+        ));
+        assert!(is_message_publish(
+            "dev__shell",
+            r#"{"command":"buzz social publish --content hi"}"#
+        ));
+        // Unrelated tools must not suppress the fallback.
+        assert!(!is_message_publish("dev__todo", "{}"));
+        assert!(!is_message_publish("dev__shell", r#"{"command":"ls -la"}"#));
+        // `read_file` ends in neither name and must not match.
+        assert!(!is_message_publish("dev__read_file", r#"{"path":"a"}"#));
+    }
+
+    #[test]
+    fn bare_acknowledgements_are_not_worth_publishing() {
+        for text in ["OK", "ok.", "Done!", "  Sure  ", "Got it", "will do", ""] {
+            assert!(
+                is_bare_acknowledgement(text),
+                "expected {text:?} to be a bare ack"
+            );
+        }
+        for text in ["12", "The capital is Paris.", "OK, the answer is 12"] {
+            assert!(
+                !is_bare_acknowledgement(text),
+                "expected {text:?} to be substantive"
+            );
+        }
+    }
+
     #[test]
     fn stop_reason_parses_all_known_values() {
         assert_eq!(StopReason::from_str("end_turn"), Some(StopReason::EndTurn));
diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs
index dab61be30a..2f8208e330 100644
--- a/crates/buzz-acp/src/config.rs
+++ b/crates/buzz-acp/src/config.rs
@@ -409,6 +409,27 @@ pub struct CliArgs {
     #[arg(long, env = "BUZZ_ACP_NO_BASE_PROMPT")]
     pub no_base_prompt: bool,
 
+    /// Offer the first-class `send_message` reply tool to the agent.
+    ///
+    /// Off by default: adding a tool changes the toolset — and so the prompt —
+    /// that every agent sees, and capable cloud models already publish their
+    /// own replies by composing a `buzz messages send` shell command. Small
+    /// local models served over shared compute deliver far more reliably with
+    /// a typed tool, so the desktop shared-compute preset opts in.
+    #[arg(long, env = "BUZZ_ACP_SEND_MESSAGE_TOOL")]
+    pub send_message_tool: bool,
+
+    /// Publish the agent's plain reply text when it ends a turn without calling
+    /// `send_message`.
+    ///
+    /// Off by default: capable models always publish their own replies, and
+    /// posting streamed text unconditionally would double-post. Small local
+    /// models served over shared compute reliably finish multi-step turns by
+    /// writing the answer as prose instead, so the desktop shared-compute
+    /// preset opts in.
+    #[arg(long, env = "BUZZ_ACP_DELIVER_PLAIN_REPLIES")]
+    pub deliver_plain_replies: bool,
+
     /// Path to a custom base prompt file. Overrides the compiled-in default.
     /// Mutually exclusive with --no-base-prompt.
     #[arg(
@@ -561,6 +582,12 @@ pub struct Config {
     /// `from_cli()`. `None` when using the compiled-in default or when
     /// `--no-base-prompt` is set.
     pub base_prompt_content: Option<String>,
+    /// Offer the first-class `send_message` reply tool. Opt-in; see the CLI
+    /// flag for rationale.
+    pub send_message_tool: bool,
+    /// Publish the agent's plain reply text when a turn ends without a
+    /// `send_message` call. Opt-in; see the CLI flag for rationale.
+    pub deliver_plain_replies: bool,
 }
 
 /// Maximum length, in characters, of a session title sent to the adapter.
@@ -1102,6 +1129,8 @@ impl Config {
             agent_owner: args.agent_owner.map(|s| s.trim().to_ascii_lowercase()),
             no_base_prompt: args.no_base_prompt,
             base_prompt_content,
+            send_message_tool: args.send_message_tool,
+            deliver_plain_replies: args.deliver_plain_replies,
         };
 
         Ok(config)
@@ -1471,6 +1500,8 @@ mod tests {
             lazy_pool: false,
             agent_owner: None,
             no_base_prompt: false,
+            send_message_tool: false,
+            deliver_plain_replies: false,
             base_prompt_content: None,
         }
     }
diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs
index d63f720c65..8970896585 100644
--- a/crates/buzz-acp/src/lib.rs
+++ b/crates/buzz-acp/src/lib.rs
@@ -1557,6 +1557,7 @@ async fn tokio_main() -> Result<()> {
             .as_deref()
             .and_then(|hex| nostr::PublicKey::from_hex(hex).ok()),
         memory_enabled: config.memory_enabled,
+        deliver_plain_replies: config.deliver_plain_replies,
         harness_name: crate::config::normalize_agent_command_identity(&config.agent_command),
         relay_url: config.relay_url.clone(),
     });
@@ -4212,6 +4213,19 @@ fn build_mcp_servers(config: &Config) -> Vec<McpServer> {
                     });
                 }
             }
+            // Offer the first-class `send_message` reply tool only where it is
+            // needed. Small local models served over shared compute deliver
+            // replies far more reliably with a typed tool than by composing a
+            // `buzz messages send` shell command; capable cloud models already
+            // publish reliably, and adding a tool changes the toolset every
+            // agent sees, so this stays scoped to the preset that measured a
+            // benefit. dev-mcp omits the tool entirely when this is unset.
+            if config.send_message_tool {
+                env.push(EnvVar {
+                    name: "BUZZ_MCP_SEND_MESSAGE_TOOL".into(),
+                    value: "1".into(),
+                });
+            }
             env
         },
     }]
@@ -5018,6 +5032,8 @@ mod build_mcp_servers_tests {
             lazy_pool: false,
             agent_owner: None,
             no_base_prompt: false,
+            send_message_tool: false,
+            deliver_plain_replies: false,
             base_prompt_content: None,
         }
     }
@@ -5239,6 +5255,8 @@ mod error_outcome_emission_tests {
             lazy_pool: false,
             agent_owner: None,
             no_base_prompt: false,
+            send_message_tool: false,
+            deliver_plain_replies: false,
             base_prompt_content: None,
         }
     }
diff --git a/crates/buzz-acp/src/pool.rs b/crates/buzz-acp/src/pool.rs
index 038f8a714c..96f3317995 100644
--- a/crates/buzz-acp/src/pool.rs
+++ b/crates/buzz-acp/src/pool.rs
@@ -543,6 +543,11 @@ pub struct PromptContext {
     /// `[Agent Memory — core]` section. On by default; disabled via
     /// `--no-memory` / `BUZZ_ACP_NO_MEMORY`.
     pub memory_enabled: bool,
+    /// Whether to publish the agent's plain reply text when a turn ends without
+    /// a `send_message` call. Off by default; the desktop shared-compute preset
+    /// opts in because small local models routinely finish a multi-step turn by
+    /// writing the answer as prose instead of calling the tool.
+    pub deliver_plain_replies: bool,
     /// Harness identity string for NIP-AM `harness` field. Derived from the
     /// configured `agent_command` at startup (e.g. `"goose"`, `"buzz-agent"`).
     pub harness_name: String,
@@ -1362,6 +1367,14 @@ pub async fn run_prompt_task(
         .as_ref()
         .map(|b| b.events.iter().map(|be| be.event.id.to_hex()).collect())
         .unwrap_or_default();
+    // Capture the reply target now: `batch` is moved into the requeue/outcome
+    // arms below, but the plain-reply fallback runs after the turn completes.
+    // Heartbeats have no batch and therefore no fallback target.
+    let plain_reply_target = if ctx.deliver_plain_replies {
+        batch.as_ref().and_then(plain_reply_target_for)
+    } else {
+        None
+    };
     agent.acp.observe(
         "turn_started",
         serde_json::json!({
@@ -2112,6 +2125,31 @@ pub async fn run_prompt_task(
                 agent.state.invalidate(&source);
             }
 
+            // Deliver the turn's plain text when the agent ended the turn
+            // without publishing a reply itself. Gated on EndTurn only:
+            // MaxTokens / MaxTurnRequests mean the turn was truncated, so the
+            // text is not a deliberate answer. `take_*` clears the buffer, so
+            // this can never post the same text twice.
+            if matches!(stop_reason, StopReason::EndTurn) {
+                if let Some(target) = plain_reply_target.as_ref() {
+                    if let Some(text) = agent.acp.take_undelivered_turn_text() {
+                        tracing::warn!(
+                            target: "pool::prompt",
+                            channel = %target.channel_id,
+                            "agent ended turn without publishing; delivering its plain text as a channel reply"
+                        );
+                        post_threaded_message(
+                            &ctx.rest_client,
+                            target.channel_id,
+                            &target.thread_tags,
+                            &text,
+                            "plain-reply delivery",
+                        )
+                        .await;
+                    }
+                }
+            }
+
             let core_stop = acp_stop_to_core(&stop_reason);
             let usage = agent.acp.take_turn_usage();
             publish_agent_turn_metric(
@@ -3584,6 +3622,34 @@ pub(crate) async fn reaction_add(rest: &crate::relay::RestClient, event_id: &str
     }
 }
 
+/// Where a plain-reply fallback should post, captured before `batch` is moved.
+#[derive(Debug, Clone)]
+pub(crate) struct PlainReplyTarget {
+    channel_id: Uuid,
+    thread_tags: ThreadTags,
+}
+
+/// Derive the plain-reply target from the batch's triggering event.
+///
+/// Anchors to the trigger's own thread when it is itself a reply; otherwise the
+/// trigger becomes the thread root, so the fallback reply lands in the same
+/// place the agent's own `send_message` would have.
+pub(crate) fn plain_reply_target_for(batch: &FlushBatch) -> Option<PlainReplyTarget> {
+    let last = batch.events.last()?;
+    let parsed = crate::queue::parse_thread_tags(&last.event);
+    let trigger_hex = last.event.id.to_hex();
+    let root = parsed.root_event_id.clone().unwrap_or(trigger_hex.clone());
+    let parent = parsed.parent_event_id.clone().unwrap_or(trigger_hex);
+    Some(PlainReplyTarget {
+        channel_id: batch.channel_id,
+        thread_tags: ThreadTags {
+            root_event_id: Some(root),
+            parent_event_id: Some(parent),
+            mentioned_pubkeys: Vec::new(),
+        },
+    })
+}
+
 /// Best-effort: post a visible failure notice (kind:9) to a channel after a
 /// batch is dead-lettered. Replies into the thread of `thread_tags` when the
 /// triggering event was threaded. Errors are logged and swallowed — the
@@ -3593,6 +3659,21 @@ pub(crate) async fn post_failure_notice(
     channel_id: Uuid,
     thread_tags: &ThreadTags,
     content: &str,
+) {
+    post_threaded_message(rest, channel_id, thread_tags, content, "failure notice").await;
+}
+
+/// Best-effort: post a threaded kind:9 message to a channel.
+///
+/// Shared by the dead-letter failure notice and the plain-reply delivery
+/// fallback; `what` only labels the log lines so the two are distinguishable.
+/// Errors are logged and swallowed — publishing must never take down the loop.
+async fn post_threaded_message(
+    rest: &crate::relay::RestClient,
+    channel_id: Uuid,
+    thread_tags: &ThreadTags,
+    content: &str,
+    what: &str,
 ) {
     let thread_ref = thread_tags.root_event_id.as_deref().and_then(|root| {
         let root_id = nostr::EventId::from_hex(root).ok()?;
@@ -3610,21 +3691,21 @@ pub(crate) async fn post_failure_notice(
         match buzz_sdk::build_message(channel_id, content, thread_ref.as_ref(), &[], false, &[]) {
             Ok(b) => b,
             Err(e) => {
-                tracing::warn!(channel = %channel_id, "failure notice: build failed: {e}");
+                tracing::warn!(channel = %channel_id, "{what}: build failed: {e}");
                 return;
             }
         };
     let event = match builder.sign_with_keys(&rest.keys) {
         Ok(e) => e,
         Err(e) => {
-            tracing::warn!(channel = %channel_id, "failure notice: sign failed: {e}");
+            tracing::warn!(channel = %channel_id, "{what}: sign failed: {e}");
             return;
         }
     };
     match tokio::time::timeout(Duration::from_secs(5), rest.submit_event(&event)).await {
         Ok(Ok(_)) => {}
-        Ok(Err(e)) => tracing::warn!(channel = %channel_id, "failure notice failed: {e}"),
-        Err(_) => tracing::warn!(channel = %channel_id, "failure notice timed out"),
+        Ok(Err(e)) => tracing::warn!(channel = %channel_id, "{what} failed: {e}"),
+        Err(_) => tracing::warn!(channel = %channel_id, "{what} timed out"),
     }
 }
 
@@ -5405,6 +5486,7 @@ mod tests {
             agent_keys: agent_keys.clone(),
             agent_owner_pubkey: owner_pubkey,
             memory_enabled: false,
+            deliver_plain_replies: false,
             harness_name: "goose".to_string(),
             relay_url: "ws://127.0.0.1:3000".to_string(),
         }
diff --git a/crates/buzz-agent/src/llm.rs b/crates/buzz-agent/src/llm.rs
index 22d3f8b73e..d7891e224e 100644
--- a/crates/buzz-agent/src/llm.rs
+++ b/crates/buzz-agent/src/llm.rs
@@ -1187,6 +1187,9 @@ fn parse_responses(v: Value) -> Result<LlmResponse, AgentError> {
         Some("completed") => ProviderStop::EndTurn,
         _ => ProviderStop::Other,
     };
+    if text.is_empty() && !reasoning.is_empty() && tool_calls.is_empty() {
+        text = reasoning.clone();
+    }
     let input_tokens = sum_usage(&v, &["input_tokens"]);
     let output_tokens = sum_usage(&v, &["output_tokens"]);
     // The Responses API nests the cache split under `input_tokens_details`.
@@ -1448,7 +1451,7 @@ fn parse_openai(v: Value) -> Result<LlmResponse, AgentError> {
     let msg = choice
         .get("message")
         .ok_or_else(|| AgentError::Llm("missing message".into()))?;
-    let (text, block_reasoning) = openai_content_parts(msg.get("content"));
+    let (mut text, block_reasoning) = openai_content_parts(msg.get("content"));
     // DeepSeek and vLLM-style OpenAI-compat hosts expose reasoning tokens on the
     // message object. Prefer `reasoning_content` (DeepSeek's field name); fall
     // back to `reasoning` (some other providers), and last to reasoning blocks
@@ -1495,6 +1498,13 @@ fn parse_openai(v: Value) -> Result<LlmResponse, AgentError> {
         }
     }
     dedupe_provider_ids(&mut tool_calls);
+    // Some OpenAI-compat hosts (and local models served via mesh) return the
+    // whole answer as reasoning with an empty `content`. With no tool calls
+    // there is nothing else to surface, so promote reasoning to text rather
+    // than reporting an empty turn.
+    if text.is_empty() && !reasoning.is_empty() && tool_calls.is_empty() {
+        text = reasoning.clone();
+    }
     let input_tokens = openai_chat_input_tokens(&v);
     let output_tokens = sum_usage(&v, &["completion_tokens"]);
     let cached_input_tokens = openai_chat_cached_tokens(&v);
@@ -2681,6 +2691,21 @@ mod tests {
         assert_eq!(r.stop, ProviderStop::EndTurn);
     }
 
+    #[test]
+    fn parse_responses_uses_reasoning_when_text_is_empty() {
+        let v = serde_json::json!({
+            "status": "completed",
+            "output": [{
+                "type": "reasoning",
+                "summary": [{"type": "summary_text", "text": "local model reply"}],
+            }],
+        });
+        let r = parse_responses(v).unwrap();
+        assert_eq!(r.text, "local model reply");
+        assert_eq!(r.reasoning, "local model reply");
+        assert!(r.tool_calls.is_empty());
+    }
+
     #[test]
     fn parse_responses_completed_with_function_call_is_tool_use() {
         let v = serde_json::json!({
@@ -4033,6 +4058,23 @@ mod tests {
         assert_eq!(parse_openai(v).unwrap().input_tokens, Some(123));
     }
 
+    #[test]
+    fn parse_openai_uses_reasoning_content_when_content_is_empty() {
+        let v = serde_json::json!({
+            "choices": [{
+                "finish_reason": "stop",
+                "message": {
+                    "content": "",
+                    "reasoning_content": "local model reply"
+                }
+            }]
+        });
+        let r = parse_openai(v).unwrap();
+        assert_eq!(r.text, "local model reply");
+        assert_eq!(r.reasoning, "local model reply");
+        assert!(r.tool_calls.is_empty());
+    }
+
     #[test]
     fn parse_openai_databricks_prompt_tokens_already_inclusive() {
         // Databricks' MLflow route uses the OpenAI chat wire format
diff --git a/crates/buzz-dev-mcp/src/lib.rs b/crates/buzz-dev-mcp/src/lib.rs
index 9b98974802..c8fcd84af2 100644
--- a/crates/buzz-dev-mcp/src/lib.rs
+++ b/crates/buzz-dev-mcp/src/lib.rs
@@ -13,6 +13,7 @@ use std::sync::Arc;
 mod paths;
 mod read_file;
 mod rg;
+mod send_message;
 mod shell;
 mod shim;
 mod str_replace;
@@ -27,13 +28,37 @@ struct DevMcp {
     tool_router: ToolRouter<DevMcp>,
 }
 
+/// Whether the harness asked for the first-class `send_message` reply tool.
+///
+/// Set by `buzz-acp` for the shared-compute preset only. Any non-empty value
+/// other than `0`/`false` enables it, so `1` and `true` both work.
+fn send_message_tool_enabled() -> bool {
+    match std::env::var("BUZZ_MCP_SEND_MESSAGE_TOOL") {
+        Ok(v) => {
+            let v = v.trim();
+            !(v.is_empty() || v == "0" || v.eq_ignore_ascii_case("false"))
+        }
+        Err(_) => false,
+    }
+}
+
 #[tool_router]
 impl DevMcp {
     fn new(state: Arc<shell::SharedState>) -> Self {
+        let mut tool_router = Self::tool_router();
+        // `send_message` is offered only when the harness asks for it via
+        // BUZZ_MCP_SEND_MESSAGE_TOOL. Adding a tool changes the toolset — and
+        // so the prompt — that every agent sees; only shared compute, where
+        // small local models need a typed reply path, has a measured benefit.
+        // Agents without it publish replies with `buzz messages send` via the
+        // shell tool, exactly as before this tool existed.
+        if !send_message_tool_enabled() {
+            tool_router.remove_route("send_message");
+        }
         Self {
             state,
             todos: Arc::new(todo::TodoState::new()),
-            tool_router: Self::tool_router(),
+            tool_router,
         }
     }
 
@@ -49,6 +74,18 @@ impl DevMcp {
         shell::run(&self.state, p, context.ct).await
     }
 
+    #[tool(
+        name = "send_message",
+        description = "Send a message to a Buzz channel. This is the ONLY way to say anything to a human: your assistant text and reasoning are never shown to anyone. Set `channel` to the channel UUID from the `[Context]` block, `content` to your message, and `reply_to` to the reply destination from `[Context]` when replying in a thread."
+    )]
+    async fn send_message(
+        &self,
+        Parameters(p): Parameters<send_message::SendMessageParams>,
+        context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
+    ) -> Result<CallToolResult, ErrorData> {
+        send_message::run(&self.state, p, context.ct).await
+    }
+
     #[tool(
         name = "read_file",
         description = "Read a text file and return its contents with line numbers. Returns lines in `{number}:{content}` format. Use `offset` (0-based) and `limit` (default 2000) to window into large files. Path resolved relative to workdir (defaults to server cwd). Prefer over cat/head/tail."
@@ -211,3 +248,50 @@ pub(crate) fn configure_no_window_async(cmd: &mut tokio::process::Command) {
     #[cfg(not(windows))]
     let _ = cmd;
 }
+
+#[cfg(test)]
+mod send_message_gate_tests {
+    use super::send_message_tool_enabled;
+
+    /// Serialize env mutation: these tests share process-global env.
+    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
+
+    fn with_var(value: Option<&str>, f: impl FnOnce()) {
+        let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
+        let prev = std::env::var("BUZZ_MCP_SEND_MESSAGE_TOOL").ok();
+        match value {
+            Some(v) => std::env::set_var("BUZZ_MCP_SEND_MESSAGE_TOOL", v),
+            None => std::env::remove_var("BUZZ_MCP_SEND_MESSAGE_TOOL"),
+        }
+        f();
+        match prev {
+            Some(v) => std::env::set_var("BUZZ_MCP_SEND_MESSAGE_TOOL", v),
+            None => std::env::remove_var("BUZZ_MCP_SEND_MESSAGE_TOOL"),
+        }
+    }
+
+    #[test]
+    fn unset_means_tool_is_not_offered() {
+        // Default for every non-shared-compute agent: toolset unchanged.
+        with_var(None, || assert!(!send_message_tool_enabled()));
+    }
+
+    #[test]
+    fn truthy_values_enable_the_tool() {
+        for v in ["1", "true", "TRUE", "yes"] {
+            with_var(Some(v), || {
+                assert!(send_message_tool_enabled(), "expected {v:?} to enable")
+            });
+        }
+    }
+
+    #[test]
+    fn falsey_and_empty_values_leave_it_disabled() {
+        // Guards against an empty env var being read as opt-in.
+        for v in ["", "  ", "0", "false", "False"] {
+            with_var(Some(v), || {
+                assert!(!send_message_tool_enabled(), "expected {v:?} to disable")
+            });
+        }
+    }
+}
diff --git a/crates/buzz-dev-mcp/src/send_message.rs b/crates/buzz-dev-mcp/src/send_message.rs
new file mode 100644
index 0000000000..41462cfae1
--- /dev/null
+++ b/crates/buzz-dev-mcp/src/send_message.rs
@@ -0,0 +1,267 @@
+//! First-class `send_message` tool: publish an agent reply to a Buzz channel.
+//!
+//! # Why this exists
+//!
+//! Before this tool, the only way for an agent to speak was to compose a
+//! `buzz messages send …` command string inside a [`crate::shell`] call. That
+//! indirection is reliable for large frontier models and unreliable for the
+//! small local models Buzz shared compute is built to serve: given the real
+//! ~3.2k-token Buzz system prompt, Gemma 4 E4B answered in prose instead of
+//! calling `shell` in 6 of 8 samples — and in several it emitted the literal
+//! command text as assistant content. Assistant text is never published, so
+//! those replies were silently dropped.
+//!
+//! Exposing the publish action as its own typed tool removes the indirection:
+//! the model picks a tool by name and fills one string field. Measured against
+//! the real system prompt and the real dev-mcp toolset, delivery went from 2/8
+//! to 8/8 on E4B and 6/6 on 26B-A4B.
+//!
+//! The tool deliberately duplicates no process machinery: it builds an argv,
+//! quotes it, and hands the command to [`crate::shell::run`], inheriting that
+//! module's timeout, output capping, cancellation, and process-group kill.
+
+use crate::shell::{self, SharedState, ShellParams};
+
+use rmcp::model::CallToolResult;
+use rmcp::ErrorData;
+use schemars::JsonSchema;
+use serde::Deserialize;
+use tokio_util::sync::CancellationToken;
+
+/// Timeout for a publish. A send is a single small relay round-trip; if it has
+/// not completed in 30s the relay is unreachable and a longer wait only stalls
+/// the turn.
+const SEND_TIMEOUT_MS: u64 = 30_000;
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct SendMessageParams {
+    /// Channel UUID to post to — the one from the `[Context]` block.
+    pub channel: String,
+    /// The message text to publish.
+    pub content: String,
+    /// Optional event id to thread this reply under. Use the reply destination
+    /// supplied in `[Context]`.
+    #[serde(default)]
+    pub reply_to: Option<String>,
+}
+
+/// Single-quote `s` for POSIX shells: wrap in `'…'` and escape embedded quotes
+/// as `'\''`. Every other byte — including newlines, `$`, backticks, and `;` —
+/// is literal inside single quotes, so this is total rather than a blocklist.
+fn shell_quote(s: &str) -> String {
+    format!("'{}'", s.replace('\'', r"'\''"))
+}
+
+/// Normalize an id the model copied out of the prompt, then validate it.
+///
+/// The `[Context]` block renders a channel as `general (#adafd400-…)`, so
+/// models routinely pass `#adafd400-…` or the whole `general (#adafd400-…)`
+/// span. Both name the right channel, and failing them would push the model
+/// back to prose — the exact failure this tool exists to remove. So a leading
+/// `#` and a surrounding `name (…)` wrapper are stripped rather than rejected.
+///
+/// Anything still not a bare id after normalization is rejected, so shell
+/// metacharacters can never reach the command line.
+fn normalize_id(kind: &str, value: &str) -> Result<String, ErrorData> {
+    let mut v = value.trim();
+    // `general (#uuid)` → `#uuid`. Only a `#`-prefixed span is unwrapped:
+    // unwrapping any parenthesized text would let `$(id)` become `id` and pass
+    // validation, turning this normalizer into a command-substitution escape.
+    if let (Some(open), Some(close)) = (v.rfind('('), v.rfind(')')) {
+        if open < close {
+            let inner = v[open + 1..close].trim();
+            if inner.starts_with('#') {
+                v = inner;
+            }
+        }
+    }
+    let v = v.strip_prefix('#').unwrap_or(v).trim();
+
+    let ok = !v.is_empty()
+        && v.len() <= 64
+        && v.chars()
+            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
+    if ok {
+        Ok(v.to_string())
+    } else {
+        Err(ErrorData::invalid_params(
+            format!("{kind} must be a bare id (alphanumeric, '-', '_'); got {value:?}"),
+            None,
+        ))
+    }
+}
+
+/// Build the `buzz messages send` command line for `p`.
+///
+/// Split out from [`run`] so the quoting and flag order are unit-testable
+/// without spawning a process.
+fn build_command(p: &SendMessageParams) -> Result<String, ErrorData> {
+    let channel = normalize_id("channel", &p.channel)?;
+    let reply_to = match &p.reply_to {
+        // An explicitly empty `reply_to` means "no thread", not a bad argument:
+        // models fill optional string fields with "" rather than omitting them.
+        Some(v) if v.trim().is_empty() => None,
+        Some(v) => Some(normalize_id("reply_to", v)?),
+        None => None,
+    };
+    if p.content.trim().is_empty() {
+        return Err(ErrorData::invalid_params(
+            "content must not be empty".to_string(),
+            None,
+        ));
+    }
+
+    let mut cmd = format!(
+        "buzz messages send --channel {} --content {}",
+        shell_quote(&channel),
+        shell_quote(&p.content)
+    );
+    if let Some(reply_to) = &reply_to {
+        cmd.push_str(&format!(" --reply-to {}", shell_quote(reply_to)));
+    }
+    Ok(cmd)
+}
+
+pub async fn run(
+    state: &SharedState,
+    p: SendMessageParams,
+    ct: CancellationToken,
+) -> Result<CallToolResult, ErrorData> {
+    let command = build_command(&p)?;
+    shell::run(
+        state,
+        ShellParams {
+            command,
+            workdir: None,
+            timeout_ms: Some(SEND_TIMEOUT_MS),
+        },
+        ct,
+    )
+    .await
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn params(channel: &str, content: &str, reply_to: Option<&str>) -> SendMessageParams {
+        SendMessageParams {
+            channel: channel.to_string(),
+            content: content.to_string(),
+            reply_to: reply_to.map(str::to_string),
+        }
+    }
+
+    #[test]
+    fn builds_plain_send() {
+        let cmd = build_command(&params("abc-123", "hello there", None)).unwrap();
+        assert_eq!(
+            cmd,
+            "buzz messages send --channel 'abc-123' --content 'hello there'"
+        );
+    }
+
+    #[test]
+    fn threads_reply_when_reply_to_present() {
+        let cmd = build_command(&params("abc-123", "hi", Some("deadbeef"))).unwrap();
+        assert_eq!(
+            cmd,
+            "buzz messages send --channel 'abc-123' --content 'hi' --reply-to 'deadbeef'"
+        );
+    }
+
+    #[test]
+    fn quotes_content_containing_single_quotes() {
+        let cmd = build_command(&params("c", "it's fine", None)).unwrap();
+        assert_eq!(
+            cmd,
+            r"buzz messages send --channel 'c' --content 'it'\''s fine'"
+        );
+    }
+
+    /// Content is attacker-influenced (it is model output, which can echo
+    /// untrusted channel text), so shell metacharacters must stay inert.
+    #[test]
+    fn neutralizes_shell_metacharacters_in_content() {
+        for payload in [
+            "; rm -rf /",
+            "$(whoami)",
+            "`id`",
+            "a && b",
+            "x | tee /tmp/y",
+            "line1\nline2",
+            "$HOME",
+        ] {
+            let cmd = build_command(&params("c", payload, None)).unwrap();
+            let quoted = shell_quote(payload);
+            assert!(
+                cmd.ends_with(&format!("--content {quoted}")),
+                "payload {payload:?} must be fully single-quoted, got {cmd}"
+            );
+            // Nothing escapes the quoted region: the only unquoted single
+            // quotes are the ones our escaping introduces.
+            assert_eq!(
+                cmd.matches('\'').count() % 2,
+                0,
+                "unbalanced quoting for {payload:?}"
+            );
+        }
+    }
+
+    #[test]
+    fn rejects_channel_with_shell_metacharacters() {
+        let err = build_command(&params("abc; rm -rf /", "hi", None)).unwrap_err();
+        assert!(err.message.contains("channel must be a bare id"));
+    }
+
+    #[test]
+    fn rejects_reply_to_with_shell_metacharacters() {
+        let err = build_command(&params("abc", "hi", Some("$(id)"))).unwrap_err();
+        assert!(err.message.contains("reply_to must be a bare id"));
+    }
+
+    #[test]
+    fn rejects_empty_channel() {
+        assert!(build_command(&params("", "hi", None)).is_err());
+    }
+
+    #[test]
+    fn rejects_blank_content() {
+        // A whitespace-only reply is never a deliberate message; failing here
+        // surfaces the mistake to the model instead of posting an empty event.
+        assert!(build_command(&params("c", "   ", None)).is_err());
+    }
+
+    #[test]
+    fn strips_hash_prefix_from_channel() {
+        // `[Context]` renders `general (#uuid)`, so models pass `#uuid`.
+        let cmd = build_command(&params("#abc-123", "hi", None)).unwrap();
+        assert_eq!(cmd, "buzz messages send --channel 'abc-123' --content 'hi'");
+    }
+
+    #[test]
+    fn unwraps_full_context_channel_span() {
+        let cmd = build_command(&params("general (#abc-123)", "hi", None)).unwrap();
+        assert_eq!(cmd, "buzz messages send --channel 'abc-123' --content 'hi'");
+    }
+
+    #[test]
+    fn treats_empty_reply_to_as_absent() {
+        // Small models fill optional string fields with "" instead of omitting.
+        let cmd = build_command(&params("abc", "hi", Some(""))).unwrap();
+        assert_eq!(cmd, "buzz messages send --channel 'abc' --content 'hi'");
+    }
+
+    #[test]
+    fn normalization_cannot_smuggle_metacharacters() {
+        // Stripping must not become an escape hatch: the inner value is still
+        // validated, so a quoted payload is rejected rather than unwrapped.
+        assert!(build_command(&params("x (#a; rm -rf /)", "hi", None)).is_err());
+        assert!(build_command(&params("#$(id)", "hi", None)).is_err());
+    }
+
+    #[test]
+    fn rejects_overlong_channel() {
+        assert!(build_command(&params(&"a".repeat(65), "hi", None)).is_err());
+    }
+}
diff --git a/crates/buzz-relay/Cargo.toml b/crates/buzz-relay/Cargo.toml
index 01f78a2d49..41bdc3b9e9 100644
--- a/crates/buzz-relay/Cargo.toml
+++ b/crates/buzz-relay/Cargo.toml
@@ -84,8 +84,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.73.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.73.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.74.0", 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.74.0", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] }
 buzz-core = { workspace = true, features = ["test-utils"] }
 buzz-auth = { workspace = true, features = ["dev"] }
 reqwest = { workspace = true }
diff --git a/crates/buzz-relay/examples/mesh_agent_e2e.rs b/crates/buzz-relay/examples/mesh_agent_e2e.rs
index b6f723f35a..7e55d07b5d 100644
--- a/crates/buzz-relay/examples/mesh_agent_e2e.rs
+++ b/crates/buzz-relay/examples/mesh_agent_e2e.rs
@@ -8,17 +8,31 @@
 //!   P2 auto-model chat      — agent sends `model: "auto"`; mesh router picks.
 //!   P3 context-fit regression — an oversized output budget (150k tokens)
 //!      must FAIL with the router's context error (proves the router's fit
-//!      gate — the failure mode the 1024 preset cap protects against).
+//!      gate — the failure mode the 4096 preset cap protects against).
 //!   P4 agentic tool use     — agent + buzz-dev-mcp writes a file on disk.
+//!   P5 Buzz reply command   — agent asks the real developer MCP to run the
+//!      exact `buzz messages send` command used to publish a reply. A fake
+//!      shell records the command at the execution boundary, so this proves
+//!      the model emits the right command but NOT that a reply lands.
+//!   P6 live relay send      — the genuine `buzz messages send` (dev-mcp's
+//!      multicall shim, real buzz-cli) against a running relay. The harness
+//!      then queries the relay itself: the agent's prose is not evidence.
+//!      Skipped unless the live-relay env vars below are set.
 //!
 //! The serve node is the same `mesh_llm_sdk::serve` path Share-compute uses
 //! (publish off, mdns, loopback). The agent legs spawn the real
 //! `buzz-agent` binary with the exact env vars the relay-mesh preset ships.
 //!
 //! Hardware-gated, not CI. Run:
-//!   cargo build --release -p buzz-agent -p buzz-dev-mcp
+//!   cargo build --release -p buzz-agent -p buzz-dev-mcp -p buzz-cli
 //!   cargo run -p buzz-relay --example mesh_agent_e2e
 //! Env: MESH_E2E_MODEL overrides the served model ref.
+//!
+//! P6 additionally needs a relay (`just relay` — Postgres + Redis) plus:
+//!   BUZZ_E2E_RELAY_URL=ws://localhost:3000
+//!   BUZZ_E2E_PRIVATE_KEY=<hex identity that may post to the channel>
+//!   BUZZ_E2E_CHANNEL=<channel uuid>
+//! All three must be set or P6 is skipped (P1-P5 still run).
 use std::process::Stdio;
 use std::time::Duration;
 
@@ -26,14 +40,43 @@ use mesh_llm_sdk::{serve, MeshDiscoveryMode};
 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
 use tokio::process::{Child, Command};
 
-// Qwen3-8B: cached GGUF *and* complete layer package on this class of
-// machine, so the serve node starts in seconds. Qwen3-30B-A3B works too but
-// mesh-llm serves it from layer packages and will download them on first
-// serve (~7GB) — fine in the app (progress UI), too slow for a smoke.
-const DEFAULT_MODEL: &str = "unsloth/Qwen3-8B-GGUF:Q4_K_M";
+// Non-reasoning Gemma 4 is the small Buzz-curated default. It reaches tool
+// calls promptly instead of spending the output budget in a hidden reasoning
+// loop. MESH_E2E_MODEL can still exercise any other served model explicitly.
+const DEFAULT_MODEL: &str = "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M";
 const API_PORT: u16 = 19437;
 const CONSOLE_PORT: u16 = 13231;
 
+#[derive(Clone)]
+struct McpServerEnv {
+    name: String,
+    value: String,
+}
+
+#[derive(Clone)]
+struct McpServerSpec {
+    name: String,
+    command: String,
+    env: Vec<McpServerEnv>,
+}
+
+impl McpServerSpec {
+    fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
+        Self {
+            name: name.into(),
+            command: command.into(),
+            env: Vec::new(),
+        }
+    }
+
+    fn set_env(&mut self, name: impl Into<String>, value: impl Into<String>) {
+        self.env.push(McpServerEnv {
+            name: name.into(),
+            value: value.into(),
+        });
+    }
+}
+
 fn main() -> anyhow::Result<()> {
     tokio::runtime::Builder::new_multi_thread()
         .enable_all()
@@ -167,7 +210,7 @@ async fn run() -> anyhow::Result<()> {
     let prompt = format!(
         "Use your developer tools to create {marker_name} in the current working directory containing exactly the text BUZZ_OK (no quotes, no newline commentary). Then confirm."
     );
-    let mcp = vec![("dev".to_string(), repo_bin("buzz-dev-mcp")?)];
+    let mcp = vec![McpServerSpec::new("dev", repo_bin("buzz-dev-mcp")?)];
     let (r, marker) =
         agent_chat_with_marker(&base, &served_id, None, &prompt, &mcp, &marker_name).await;
     let file_ok = std::fs::read_to_string(&marker)
@@ -187,6 +230,83 @@ async fn run() -> anyhow::Result<()> {
     }
     let _ = std::fs::remove_file(&marker);
 
+    // P5: prove the model follows Buzz's real reply path. buzz-dev-mcp always
+    // prepends its own multicall `buzz` shim to PATH, so a fake `buzz` later on
+    // PATH cannot isolate this test. Instead, inject a fake shell into the real
+    // MCP server. It records the exact command at the execution boundary and
+    // returns the same accepted-write shape as buzz-cli, without contacting a
+    // community.
+    let send_marker = format!("mesh-e2e-buzz-send-{}.txt", std::process::id());
+    let prompt = "Use the developer shell tool to run exactly this command: buzz messages send --channel test-channel --content BUZZ_REPLY_OK. Do not simulate or substitute another command. After it succeeds, confirm briefly.";
+    let (r, marker) =
+        agent_chat_with_fake_shell(&base, &served_id, prompt, &mcp, &send_marker).await;
+    let recorded = std::fs::read_to_string(&marker).unwrap_or_default();
+    // The fake shell appends every command it is handed. Small models often
+    // probe with an unrelated command first, so the pass condition is that the
+    // exact Buzz send reached the execution boundary on some line — not that it
+    // was the only command attempted.
+    const EXPECTED_SEND: &str = "buzz messages send --channel test-channel --content BUZZ_REPLY_OK";
+    let attempts: Vec<&str> = recorded
+        .lines()
+        .map(str::trim)
+        .filter(|line| !line.is_empty())
+        .collect();
+    let send_ok = attempts.contains(&EXPECTED_SEND);
+    match r {
+        Ok(text) => record(
+            "P5 Buzz reply command",
+            send_ok,
+            format!("commands attempted {attempts:?}; agent said: {text}"),
+        ),
+        Err(e) => record(
+            "P5 Buzz reply command",
+            send_ok,
+            format!("agent error: {e}; commands attempted: {attempts:?}"),
+        ),
+    }
+    let _ = std::fs::remove_file(&marker);
+
+    // P6: live relay end-to-end. P5 stops at the shell boundary with a fake
+    // shell, so it cannot prove a reply ever reaches a channel. Here the agent
+    // runs the genuine `buzz messages send` (dev-mcp's multicall shim, real
+    // buzz-cli code) against a running relay, and the harness independently
+    // queries the relay afterwards rather than trusting the agent's prose.
+    //
+    // Infra-gated: needs `just relay` (Postgres + Redis) and an identity:
+    //   BUZZ_E2E_RELAY_URL, BUZZ_E2E_PRIVATE_KEY, BUZZ_E2E_CHANNEL
+    match (
+        std::env::var("BUZZ_E2E_RELAY_URL").ok(),
+        std::env::var("BUZZ_E2E_PRIVATE_KEY").ok(),
+        std::env::var("BUZZ_E2E_CHANNEL").ok(),
+    ) {
+        (Some(relay_url), Some(private_key), Some(channel)) => {
+            let live_marker = format!("BUZZ_LIVE_OK_{}", std::process::id());
+            let mut dev = McpServerSpec::new("dev", repo_bin("buzz-dev-mcp")?);
+            dev.set_env("BUZZ_RELAY_URL", relay_url.as_str());
+            dev.set_env("BUZZ_PRIVATE_KEY", private_key.as_str());
+            let prompt = format!(
+                "You are a Buzz agent with shell access. The `buzz` CLI is on your PATH; `buzz messages send` publishes to a channel. Post a message containing exactly the token {live_marker} to channel {channel}."
+            );
+            let r = agent_chat(&base, &served_id, None, &prompt, &[dev]).await;
+            let landed = relay_has_message(&relay_url, &private_key, &channel, &live_marker).await;
+            let landed_ok = matches!(landed, Ok(true));
+            let detail = match (&r, &landed) {
+                (Ok(text), Ok(true)) => {
+                    format!("relay returned {live_marker}; agent said: {text}")
+                }
+                (Ok(text), Ok(false)) => {
+                    format!("{live_marker} never reached the relay; agent said: {text}")
+                }
+                (Ok(text), Err(e)) => format!("relay verification failed: {e}; agent said: {text}"),
+                (Err(e), _) => format!("agent error: {e}"),
+            };
+            record("P6 live relay send", landed_ok, detail);
+        }
+        _ => eprintln!(
+            "[e2e] SKIP P6 live relay send (set BUZZ_E2E_RELAY_URL, BUZZ_E2E_PRIVATE_KEY, BUZZ_E2E_CHANNEL)"
+        ),
+    }
+
     eprintln!("[e2e] {pass} passed, {fail} failed");
     if fail > 0 {
         eprintln!("[e2e] FAIL: {fail} permutation(s) failed");
@@ -220,6 +340,45 @@ fn repo_bin(name: &str) -> anyhow::Result<String> {
     Ok(path.to_string_lossy().into_owned())
 }
 
+/// Ask the relay directly whether `marker` is present in `channel`.
+///
+/// This is the independent check that makes P6 meaningful: the agent's prose is
+/// not evidence, and neither is the shell exit code. The harness performs its
+/// own authenticated read against the relay in a separate process and only
+/// trusts what comes back. Polls because the relay persists and fans out
+/// asynchronously, so an immediate read can race the write.
+async fn relay_has_message(
+    relay_url: &str,
+    private_key: &str,
+    channel: &str,
+    marker: &str,
+) -> anyhow::Result<bool> {
+    let buzz = repo_bin("buzz")?;
+    for _ in 0..15 {
+        let output = Command::new(&buzz)
+            .args([
+                "--format",
+                "compact",
+                "messages",
+                "get",
+                "--channel",
+                channel,
+                "--limit",
+                "50",
+            ])
+            .env("PATH", std::env::var("PATH").unwrap_or_default())
+            .env("BUZZ_RELAY_URL", relay_url)
+            .env("BUZZ_PRIVATE_KEY", private_key)
+            .output()
+            .await?;
+        if output.status.success() && String::from_utf8_lossy(&output.stdout).contains(marker) {
+            return Ok(true);
+        }
+        tokio::time::sleep(Duration::from_secs(1)).await;
+    }
+    Ok(false)
+}
+
 /// Spawn the real buzz-agent with relay-mesh preset env and drive one ACP
 /// session/prompt over stdio. Returns the concatenated agent message text,
 /// or Err carrying the agent's error message.
@@ -228,10 +387,11 @@ async fn agent_chat(
     model: &str,
     max_output_tokens: Option<&str>,
     prompt: &str,
-    mcp_servers: &[(String, String)],
+    mcp_servers: &[McpServerSpec],
 ) -> anyhow::Result<String> {
     let (result, _) =
-        agent_chat_in_isolated_home(base, model, max_output_tokens, prompt, mcp_servers).await;
+        agent_chat_in_isolated_home(base, model, max_output_tokens, prompt, mcp_servers, None)
+            .await;
     result
 }
 
@@ -240,11 +400,25 @@ async fn agent_chat_with_marker(
     model: &str,
     max_output_tokens: Option<&str>,
     prompt: &str,
-    mcp_servers: &[(String, String)],
+    mcp_servers: &[McpServerSpec],
+    marker_name: &str,
+) -> (anyhow::Result<String>, std::path::PathBuf) {
+    let (result, home) =
+        agent_chat_in_isolated_home(base, model, max_output_tokens, prompt, mcp_servers, None)
+            .await;
+    (result, home.join(marker_name))
+}
+
+async fn agent_chat_with_fake_shell(
+    base: &str,
+    model: &str,
+    prompt: &str,
+    mcp_servers: &[McpServerSpec],
     marker_name: &str,
 ) -> (anyhow::Result<String>, std::path::PathBuf) {
     let (result, home) =
-        agent_chat_in_isolated_home(base, model, max_output_tokens, prompt, mcp_servers).await;
+        agent_chat_in_isolated_home(base, model, None, prompt, mcp_servers, Some(marker_name))
+            .await;
     (result, home.join(marker_name))
 }
 
@@ -253,7 +427,8 @@ async fn agent_chat_in_isolated_home(
     model: &str,
     max_output_tokens: Option<&str>,
     prompt: &str,
-    mcp_servers: &[(String, String)],
+    mcp_servers: &[McpServerSpec],
+    fake_shell_marker_name: Option<&str>,
 ) -> (anyhow::Result<String>, std::path::PathBuf) {
     let agent = match repo_bin("buzz-agent") {
         Ok(agent) => agent,
@@ -265,10 +440,28 @@ async fn agent_chat_in_isolated_home(
         return (Err(error.into()), home);
     }
 
+    let child_path = std::env::var("PATH").unwrap_or_default();
+    let mut effective_mcp_servers = mcp_servers.to_vec();
+    if let Some(marker_name) = fake_shell_marker_name {
+        let fake_shell = match install_fake_shell(&home) {
+            Ok(fake_shell) => fake_shell,
+            Err(error) => return (Err(error), home),
+        };
+        let marker = home.join(marker_name);
+        let Some(dev_server) = effective_mcp_servers
+            .iter_mut()
+            .find(|server| server.name == "dev")
+        else {
+            return (Err(anyhow::anyhow!("P5 requires the dev MCP server")), home);
+        };
+        dev_server.set_env("BUZZ_SHELL", fake_shell.to_string_lossy());
+        dev_server.set_env("BUZZ_E2E_SEND_MARKER", marker.to_string_lossy());
+    }
+
     let mut command = Command::new(&agent);
     command
         .env_clear()
-        .env("PATH", std::env::var("PATH").unwrap_or_default())
+        .env("PATH", child_path)
         .env("HOME", &home)
         // Exactly the environment apply_relay_mesh_env() supplies.
         .env("BUZZ_AGENT_PROVIDER", "openai")
@@ -278,7 +471,9 @@ async fn agent_chat_in_isolated_home(
         .env("OPENAI_COMPAT_API_KEY", "buzz-mesh-local")
         .env("OPENAI_COMPAT_API", "chat")
         .env("BUZZ_AGENT_MAX_OUTPUT_TOKENS", "4096")
-        .env("BUZZ_AGENT_THINKING_EFFORT", "none")
+        // No BUZZ_AGENT_THINKING_EFFORT: apply_relay_mesh_env() deliberately
+        // leaves it unset so each model's chat template picks its own default.
+        // Pinning a value here would test a config the product does not ship.
         .stdin(Stdio::piped())
         .stdout(Stdio::piped())
         .stderr(Stdio::null());
@@ -293,15 +488,69 @@ async fn agent_chat_in_isolated_home(
         Err(error) => return (Err(error.into()), home),
     };
 
-    let result = drive_acp(&mut child, prompt, mcp_servers, &home).await;
+    let result = drive_acp(&mut child, prompt, &effective_mcp_servers, &home).await;
     let _ = child.kill().await;
     (result, home)
 }
 
+#[cfg(unix)]
+fn install_fake_shell(home: &std::path::Path) -> anyhow::Result<std::path::PathBuf> {
+    use std::os::unix::fs::PermissionsExt;
+
+    let bin_dir = home.join("bin");
+    std::fs::create_dir_all(&bin_dir)?;
+    let executable = bin_dir.join("mesh-e2e-shell");
+    // buzz-dev-mcp spawns `<shell> -c "<command>"` on Unix (`shell_flag()`
+    // returns `-c` for every non-cmd/pwsh shell). Accept `-lc` too so a future
+    // login-shell dialect change does not silently stop recording.
+    // Every command is appended, one per line: a small model often probes with
+    // an unrelated command first, and overwriting would discard the real send.
+    std::fs::write(
+        &executable,
+        r#"#!/bin/sh
+case "$1" in
+  -c|-lc) command_string="$2" ;;
+  *)
+    printf 'unsupported fake shell invocation: %s\n' "$*" >&2
+    exit 2
+    ;;
+esac
+printf '%s\n' "$command_string" >> "$BUZZ_E2E_SEND_MARKER"
+if [ "$command_string" = "buzz messages send --channel test-channel --content BUZZ_REPLY_OK" ]; then
+  printf '%s\n' '{"event_id":"mesh-e2e-event","accepted":true,"message":"ok"}'
+  exit 0
+fi
+printf 'unsupported fake shell command: %s\n' "$command_string" >&2
+exit 2
+"#,
+    )?;
+    let mut permissions = std::fs::metadata(&executable)?.permissions();
+    permissions.set_mode(0o755);
+    std::fs::set_permissions(&executable, permissions)?;
+    // BUZZ_SHELL must name the executable itself. Returning the directory made
+    // `resolve_bash()` reject it as "not a file" and fall back to real bash, so
+    // the fake shell never ran and P5 could never record a command.
+    Ok(executable)
+}
+
+#[cfg(not(unix))]
+fn install_fake_shell(_home: &std::path::Path) -> anyhow::Result<std::path::PathBuf> {
+    anyhow::bail!("the mesh agent hardware harness currently requires Unix")
+}
+
+/// The real Buzz system prompt every managed agent receives, straight from the
+/// harness crate rather than a paraphrase.
+///
+/// A terse hand-written test prompt is not a proxy for this: it is ~3.2k tokens
+/// of instructions, and the reply-delivery rules the model must follow to get a
+/// message into a channel live in here. Testing with a short prompt measures a
+/// toy, not Buzz.
+const BUZZ_BASE_PROMPT: &str = include_str!("../../buzz-acp/src/base_prompt.md");
+
 async fn drive_acp(
     child: &mut Child,
     prompt: &str,
-    mcp_servers: &[(String, String)],
+    mcp_servers: &[McpServerSpec],
     cwd: &std::path::Path,
 ) -> anyhow::Result<String> {
     let mut stdin = child
@@ -316,8 +565,18 @@ async fn drive_acp(
 
     let mcp_json: Vec<serde_json::Value> = mcp_servers
         .iter()
-        .map(|(name, command)| {
-            serde_json::json!({ "name": name, "command": command, "args": [], "env": [] })
+        .map(|server| {
+            let env: Vec<serde_json::Value> = server
+                .env
+                .iter()
+                .map(|entry| serde_json::json!({ "name": &entry.name, "value": &entry.value }))
+                .collect();
+            serde_json::json!({
+                "name": &server.name,
+                "command": &server.command,
+                "args": [],
+                "env": env,
+            })
         })
         .collect();
 
@@ -338,7 +597,7 @@ async fn drive_acp(
                 "params": {
                     "cwd": cwd.to_string_lossy(),
                     "mcpServers": mcp_json,
-                    "systemPrompt": "You are a terse test agent. Follow instructions exactly."
+                    "systemPrompt": BUZZ_BASE_PROMPT
                 }
             }))
             .as_bytes(),
diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index d4f7a4a2d4..49e6ef9fba 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -75,6 +75,19 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if 1.0.4",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.4"
@@ -1508,7 +1521,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]]
@@ -1745,7 +1758,7 @@ dependencies = [
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
- "windows 0.61.3",
+ "windows 0.62.2",
 ]
 
 [[package]]
@@ -2149,7 +2162,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]]
@@ -3099,8 +3112,8 @@ dependencies = [
  "libc",
  "log",
  "rustversion",
- "windows-link 0.1.3",
- "windows-result 0.3.4",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
 ]
 
 [[package]]
@@ -3845,7 +3858,7 @@ dependencies = [
  "tokio",
  "tower-service",
  "tracing",
- "windows-registry 0.5.3",
+ "windows-registry 0.6.1",
 ]
 
 [[package]]
@@ -3860,7 +3873,7 @@ dependencies = [
  "js-sys",
  "log",
  "wasm-bindgen",
- "windows-core 0.61.2",
+ "windows-core 0.62.2",
 ]
 
 [[package]]
@@ -4936,8 +4949,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-api-client"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "hex",
  "mesh-llm-client",
@@ -4946,8 +4959,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-api-server"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-api-client",
@@ -4957,13 +4970,13 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-build-info"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 
 [[package]]
 name = "mesh-llm-client"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -4994,8 +5007,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-config"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -5010,8 +5023,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-embedded-runtime"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-host-runtime",
@@ -5020,8 +5033,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-events"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "clap",
@@ -5032,8 +5045,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-gpu-bench"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "cc",
@@ -5045,8 +5058,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-guardrails"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "serde",
  "serde_json",
@@ -5054,16 +5067,16 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-hardware-profile"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "mesh-llm-native-runtime",
 ]
 
 [[package]]
 name = "mesh-llm-host-runtime"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "argon2",
@@ -5085,7 +5098,6 @@ dependencies = [
  "http",
  "http-body-util",
  "httparse",
- "if-addrs",
  "iroh",
  "json5",
  "keyring",
@@ -5133,6 +5145,7 @@ dependencies = [
  "serde_yaml",
  "sha2 0.10.9",
  "skippy-coordinator",
+ "skippy-ffi",
  "skippy-protocol",
  "skippy-runtime",
  "skippy-server",
@@ -5155,8 +5168,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-identity"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "argon2",
  "base64 0.22.1",
@@ -5177,8 +5190,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-native-runtime"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "serde",
@@ -5188,8 +5201,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-node"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-types",
@@ -5202,8 +5215,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-plugin"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5219,8 +5232,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-plugin-manager"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -5230,6 +5243,7 @@ dependencies = [
  "reqwest 0.12.28",
  "serde",
  "serde_json",
+ "sha2 0.10.9",
  "tar",
  "tempfile",
  "zip 2.4.2",
@@ -5237,8 +5251,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-protocol"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "hex",
@@ -5250,16 +5264,16 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-routing"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "iroh",
 ]
 
 [[package]]
 name = "mesh-llm-runtime-install"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -5281,8 +5295,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-sdk"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "mesh-llm-api-client",
@@ -5296,8 +5310,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-skills"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "dirs",
@@ -5307,8 +5321,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-system"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "chrono",
@@ -5330,8 +5344,8 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-types"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "hex",
  "serde",
@@ -5341,13 +5355,13 @@ dependencies = [
 
 [[package]]
 name = "mesh-llm-ui"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 
 [[package]]
 name = "mesh-mixture-of-agents"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "async-trait",
  "mesh-llm-guardrails",
@@ -5419,8 +5433,8 @@ dependencies = [
 
 [[package]]
 name = "model-artifact"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5430,8 +5444,8 @@ dependencies = [
 
 [[package]]
 name = "model-hf"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5448,8 +5462,8 @@ dependencies = [
 
 [[package]]
 name = "model-package"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "bytes",
@@ -5468,16 +5482,16 @@ dependencies = [
 
 [[package]]
 name = "model-ref"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "model-resolver"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "model-artifact",
@@ -6131,7 +6145,7 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
 dependencies = [
- "proc-macro-crate 1.3.1",
+ "proc-macro-crate 2.0.2",
  "proc-macro2",
  "quote",
  "syn 2.0.118",
@@ -6505,8 +6519,8 @@ dependencies = [
 
 [[package]]
 name = "openai-frontend"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "async-trait",
  "axum",
@@ -6699,7 +6713,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]]
@@ -7409,7 +7423,7 @@ version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
 dependencies = [
- "heck 0.4.1",
+ "heck 0.5.0",
  "itertools",
  "log",
  "multimap",
@@ -9038,8 +9052,8 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
 
 [[package]]
 name = "skippy-cache"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "blake3",
@@ -9048,29 +9062,29 @@ dependencies = [
 
 [[package]]
 name = "skippy-coordinator"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "thiserror 2.0.18",
 ]
 
 [[package]]
 name = "skippy-ffi"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "libloading 0.8.9",
 ]
 
 [[package]]
 name = "skippy-metrics"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 
 [[package]]
 name = "skippy-protocol"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "prost",
  "prost-build",
@@ -9080,8 +9094,8 @@ dependencies = [
 
 [[package]]
 name = "skippy-runtime"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "anyhow",
  "libc",
@@ -9094,9 +9108,10 @@ dependencies = [
 
 [[package]]
 name = "skippy-server"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
+ "ahash",
  "anyhow",
  "async-trait",
  "axum",
@@ -9122,8 +9137,8 @@ dependencies = [
 
 [[package]]
 name = "skippy-topology"
-version = "0.73.1"
-source = "git+https://github.com/Mesh-LLM/mesh-llm.git?rev=f455d493a2ae82baf2a326e2d0fda351433b4b30#f455d493a2ae82baf2a326e2d0fda351433b4b30"
+version = "0.74.0"
+source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1"
 dependencies = [
  "serde",
  "serde_json",
@@ -10150,7 +10165,7 @@ 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.61.2",
@@ -11786,7 +11801,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]]
@@ -12415,8 +12430,8 @@ dependencies = [
  "log",
  "serde",
  "thiserror 2.0.18",
- "windows 0.61.3",
- "windows-core 0.61.2",
+ "windows 0.62.2",
+ "windows-core 0.62.2",
 ]
 
 [[package]]
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index 8bb643fea3..c44568d983 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -90,14 +90,14 @@ buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona
 buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
 buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" }
 iroh = { version = "1.0.2", optional = true }
-mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
-mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
+mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", 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.74.0", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
 # Model catalog + hardware survey for the Share-compute model picker (same
 # diagnose pattern as mesh-console). Lib name of mesh-llm-client is mesh_client.
-mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-client", optional = true }
-mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-node", optional = true }
-mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-system", optional = true }
-mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-events", optional = true }
+mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", package = "mesh-llm-client", optional = true }
+mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", package = "mesh-llm-node", optional = true }
+mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", package = "mesh-llm-system", optional = true }
+mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.74.0", 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 305c54a203..998bc6e7d2 100644
--- a/desktop/src-tauri/src/commands/mesh_llm.rs
+++ b/desktop/src-tauri/src/commands/mesh_llm.rs
@@ -5,12 +5,35 @@ use tauri::{AppHandle, Manager, State};
 
 use crate::{app_state::AppState, mesh_llm, relay};
 
-#[derive(Debug, serde::Deserialize, serde::Serialize)]
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
 #[serde(rename_all = "camelCase")]
 struct MeshSharingConfig {
     enabled: bool,
+    /// A fresh Share Compute request that must cross a process boundary before
+    /// it can start. Consumed before startup so an interrupted download is not
+    /// resumed on a later launch.
+    #[serde(default)]
+    start_on_next_launch: bool,
     model_id: String,
     max_vram_gb: Option<u64>,
+    /// Community relay where Share Compute was explicitly enabled. Older
+    /// configs predate community binding and restore against the active relay.
+    #[serde(default)]
+    relay_url: Option<String>,
+}
+
+fn pending_new_start_checkpoint(config: &MeshSharingConfig) -> MeshSharingConfig {
+    let mut checkpoint = config.clone();
+    checkpoint.enabled = false;
+    checkpoint.start_on_next_launch = false;
+    checkpoint
+}
+
+fn one_shot_restart_checkpoint(config: &MeshSharingConfig) -> MeshSharingConfig {
+    let mut checkpoint = config.clone();
+    checkpoint.enabled = false;
+    checkpoint.start_on_next_launch = true;
+    checkpoint
 }
 
 fn mesh_sharing_config_path(app: &AppHandle) -> Result<PathBuf, String> {
@@ -89,8 +112,10 @@ fn sharing_config_from_request(
         .ok_or_else(|| "modelId is required for serve mode".to_string())?;
     Ok(MeshSharingConfig {
         enabled: true,
+        start_on_next_launch: false,
         model_id: model_id.to_string(),
         max_vram_gb: request.max_vram_gb,
+        relay_url: request.relay_url.clone(),
     })
 }
 
@@ -117,7 +142,7 @@ fn restart_to_share(
     app: &AppHandle,
     config: &MeshSharingConfig,
 ) -> CmdResult<mesh_llm::MeshNodeStatus> {
-    save_mesh_sharing_config(app, config)?;
+    save_mesh_sharing_config(app, &one_shot_restart_checkpoint(config))?;
     let status = restarting_share_status(config);
     app.request_restart();
     Ok(status)
@@ -133,7 +158,7 @@ fn buzz_mesh_name_for_relay(relay_url: &str) -> String {
     format!("buzz-community-{}", &digest[..32])
 }
 
-fn buzz_mesh_name(state: &AppState) -> String {
+pub(super) fn buzz_mesh_name(state: &AppState) -> String {
     buzz_mesh_name_for_relay(&relay::relay_ws_url_with_override(state))
 }
 
@@ -150,8 +175,13 @@ fn advance_mesh_status_cursor(
     Ok(cursor)
 }
 
-async fn query_mesh_discovery_events(state: &AppState) -> Result<Vec<nostr::Event>, String> {
-    let mut events = relay::query_relay(state, &[mesh_llm::relay_membership_filter()]).await?;
+async fn query_mesh_discovery_events_at(
+    state: &AppState,
+    relay_url: &str,
+) -> Result<Vec<nostr::Event>, String> {
+    let api_base_url = relay::relay_http_base_url(relay_url);
+    let mut events =
+        relay::query_relay_at(state, &api_base_url, &[mesh_llm::relay_membership_filter()]).await?;
     let member_pubkeys = mesh_llm::current_member_pubkeys(&events);
     if member_pubkeys.is_empty() {
         // Distinguish "relay returned a membership snapshot listing zero
@@ -172,7 +202,7 @@ async fn query_mesh_discovery_events(state: &AppState) -> Result<Vec<nostr::Even
     let mut previous_cursor: Option<(u64, String)> = None;
 
     loop {
-        let page = relay::query_relay(state, &[status_filter.clone()]).await?;
+        let page = relay::query_relay_at(state, &api_base_url, &[status_filter.clone()]).await?;
         let done = page.len() < mesh_llm::MESH_STATUS_PAGE_SIZE;
         if !done {
             let cursor = advance_mesh_status_cursor(&mut status_filter, &page)?;
@@ -188,6 +218,10 @@ async fn query_mesh_discovery_events(state: &AppState) -> Result<Vec<nostr::Even
     }
 }
 
+async fn query_mesh_discovery_events(state: &AppState) -> Result<Vec<nostr::Event>, String> {
+    query_mesh_discovery_events_at(state, &relay::relay_ws_url_with_override(state)).await
+}
+
 /// Resolve the admission roster by intersecting member-signed mesh status
 /// reporters with the current NIP-43 direct-member list.
 ///
@@ -201,6 +235,14 @@ pub(crate) async fn resolve_trusted_owner_ids(state: &AppState) -> Result<Vec<St
     Ok(mesh_llm::owner_ids_from_events(&events))
 }
 
+pub(crate) async fn resolve_trusted_owner_ids_at(
+    state: &AppState,
+    relay_url: &str,
+) -> Result<Vec<String>, String> {
+    let events = query_mesh_discovery_events_at(state, relay_url).await?;
+    Ok(mesh_llm::owner_ids_from_events(&events))
+}
+
 /// Resolve the roster for an initial node *start*, failing closed to self-only
 /// (an empty roster) when the relay query fails. This is safe only at start:
 /// there is no established allowlist to preserve yet. The periodic
@@ -244,10 +286,11 @@ fn buzz_mesh_join_targets(
 /// Resolve the validated member endpoint this runtime should join to enter the
 /// existing Buzz community mesh. `Ok(None)` means this machine is the first
 /// live serving member (or is itself the shared bootstrap contact).
-pub(crate) async fn resolve_buzz_mesh_join_targets(
+pub(crate) async fn resolve_buzz_mesh_join_targets_at(
     state: &AppState,
+    relay_url: &str,
 ) -> Result<Vec<mesh_llm::MeshServeTarget>, String> {
-    let events = query_mesh_discovery_events(state).await?;
+    let events = query_mesh_discovery_events_at(state, relay_url).await?;
     let self_owner_id = mesh_llm::ensure_owner_identity()
         .map_err(|error| format!("failed to load mesh owner identity: {error}"))?
         .owner_id;
@@ -261,8 +304,11 @@ pub(crate) async fn resolve_buzz_mesh_join_targets(
 /// snapshot. A node start used to repeat the full membership + status query
 /// for each value, making Share Compute startup both slower and more exposed
 /// to inconsistent snapshots.
-async fn resolve_buzz_mesh_startup(state: &AppState) -> (Vec<String>, Option<String>) {
-    match query_mesh_discovery_events(state).await {
+async fn resolve_buzz_mesh_startup_at(
+    state: &AppState,
+    relay_url: &str,
+) -> (Vec<String>, Option<String>) {
+    match query_mesh_discovery_events_at(state, relay_url).await {
         Ok(events) => {
             let trusted_owner_ids = mesh_llm::owner_ids_from_events(&events);
             let join_token = mesh_llm::ensure_owner_identity()
@@ -291,32 +337,60 @@ async fn resolve_buzz_mesh_startup(state: &AppState) -> (Vec<String>, Option<Str
 }
 
 pub(crate) async fn restore_mesh_sharing(app: &AppHandle, state: &AppState) -> CmdResult<()> {
-    let Some(config) = load_mesh_sharing_config(app)? else {
+    let Some(mut config) = load_mesh_sharing_config(app)? else {
         return Ok(());
     };
-    if !config.enabled || config.model_id.trim().is_empty() {
+    if (!config.enabled && !config.start_on_next_launch) || config.model_id.trim().is_empty() {
         return Ok(());
     }
+    config.model_id = mesh_llm::canonical_curated_model_id(&config.model_id).to_string();
     if state.mesh_llm_runtime.lock().await.is_some() {
         return Ok(());
     }
-    let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup(state).await;
+    let relay_url = config
+        .relay_url
+        .clone()
+        .unwrap_or_else(|| relay::relay_ws_url_with_override(state));
+    let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup_at(state, &relay_url).await;
     let mut runtime = state.mesh_llm_runtime.lock().await;
     if runtime.is_some() {
         return Ok(());
     }
+    if config.start_on_next_launch {
+        // Consume a role-switch request before doing any potentially long model
+        // work. If Buzz exits during that work, the next launch stays stopped.
+        config = pending_new_start_checkpoint(&config);
+        save_mesh_sharing_config(app, &config)?;
+    }
+    // This is restoration of a previously inference-ready serving node. Keep
+    // the enabled checkpoint armed while restoring so a transient startup
+    // failure does not silently turn Share Compute off. New starts remain
+    // disarmed in `mesh_start_node` until their first inference probe passes.
     let request = mesh_llm::StartMeshNodeRequest {
         mode: mesh_llm::MeshNodeMode::Serve,
-        model_id: Some(config.model_id),
+        model_id: Some(config.model_id.clone()),
         max_vram_gb: config.max_vram_gb,
         join_token,
-        mesh_name: Some(buzz_mesh_name(state)),
+        mesh_name: Some(buzz_mesh_name_for_relay(&relay_url)),
+        relay_url: Some(relay_url),
         trusted_owner_ids: Some(trusted_owner_ids),
     };
     let started = mesh_llm::DesktopMeshRuntime::start(request)
         .await
         .map_err(|error| format!("failed to restore Share Compute: {error:#}"))?;
+    if let Err(error) = wait_for_mesh_inference(&config.model_id).await {
+        let cleanup = started.stop().await;
+        if let Err(cleanup_error) = cleanup {
+            eprintln!(
+                "buzz-mesh: restored node failed inference readiness and cleanup was incomplete: {cleanup_error:#}"
+            );
+        }
+        return Err(format!("failed to restore Share Compute: {error}"));
+    }
     *runtime = Some(started);
+    config.enabled = true;
+    config.start_on_next_launch = false;
+    save_mesh_sharing_config(app, &config)?;
     drop(runtime);
     mesh_llm::publish_current_status_once(app, "restore").await;
     Ok(())
@@ -328,6 +402,11 @@ pub async fn mesh_start_node(
     state: State<'_, AppState>,
     mut request: mesh_llm::StartMeshNodeRequest,
 ) -> CmdResult<mesh_llm::MeshNodeStatus> {
+    let relay_url = relay::relay_ws_url_with_override(&state);
+    request.relay_url = Some(relay_url.clone());
+    if let Some(model_id) = request.model_id.as_mut() {
+        *model_id = mesh_llm::canonical_curated_model_id(model_id).to_string();
+    }
     let sharing_config = if request.mode == mesh_llm::MeshNodeMode::Serve {
         Some(sharing_config_from_request(&request)?)
     } else {
@@ -362,13 +441,14 @@ pub async fn mesh_start_node(
     // Frontend requests never carry a roster. Resolve it and the bootstrap
     // endpoint from one snapshot so UI startup does not repeat relay probes.
     if request.trusted_owner_ids.is_none() || request.join_token.is_none() {
-        let (trusted_owner_ids, join_token) = resolve_buzz_mesh_startup(&state).await;
+        let (trusted_owner_ids, join_token) =
+            resolve_buzz_mesh_startup_at(&state, &relay_url).await;
         request.trusted_owner_ids.get_or_insert(trusted_owner_ids);
         if request.join_token.is_none() {
             request.join_token = join_token;
         }
     }
-    request.mesh_name = Some(buzz_mesh_name(&state));
+    request.mesh_name = Some(buzz_mesh_name_for_relay(&relay_url));
     let mut runtime = state.mesh_llm_runtime.lock().await;
 
     let plan = match runtime.as_ref() {
@@ -386,6 +466,13 @@ pub async fn mesh_start_node(
         return Err("mesh node is already running".to_string());
     }
 
+    if let Some(config) = sharing_config.as_ref() {
+        // Do not arm launch restoration until the exact inference path used by
+        // agents succeeds. Mesh may bind its ports after primary weights load
+        // while package layers are still downloading.
+        save_mesh_sharing_config(&app, &pending_new_start_checkpoint(config))?;
+    }
+
     let started = mesh_llm::DesktopMeshRuntime::start(request)
         .await
         .map_err(|error| format!("{error:#}"))?;
@@ -409,6 +496,21 @@ pub async fn mesh_start_node(
             ));
         }
     };
+    if let Some(config) = sharing_config.as_ref() {
+        if let Err(error) = wait_for_mesh_inference(&config.model_id).await {
+            let cleanup = started.stop().await;
+            if let Err(cleanup_error) = &cleanup {
+                eprintln!(
+                    "buzz-mesh: started node failed inference readiness and cleanup was incomplete: {cleanup_error:#}"
+                );
+            }
+            drop(runtime);
+            app.request_restart();
+            return Err(format!(
+                "mesh node started but inference never became ready: {error}; Buzz is restarting to guarantee cleanup"
+            ));
+        }
+    }
     *runtime = Some(started);
     drop(runtime);
     if let Some(config) = sharing_config.as_ref() {
@@ -612,6 +714,7 @@ pub(crate) async fn ensure_client_node_for_model(
         max_vram_gb: None,
         join_token: Some(join_token.clone()),
         mesh_name: Some(buzz_mesh_name(state)),
+        relay_url: Some(relay::relay_ws_url_with_override(state)),
         trusted_owner_ids: Some(resolve_trusted_owner_ids_or_self_only(state).await),
     };
     let mut runtime = state.mesh_llm_runtime.lock().await;
@@ -753,6 +856,18 @@ pub(crate) async fn ensure_relay_mesh_for_record(
             }
         }
     }
+
+    // A persisted Share Compute configuration is authoritative about this
+    // machine's role. If no runtime is currently tracked (for example after a
+    // clean process restart), restore the serving node instead of treating an
+    // agent request as permission to replace it with a client node.
+    if load_mesh_sharing_config(app)?
+        .is_some_and(|config| config.enabled && !config.model_id.trim().is_empty())
+    {
+        restore_mesh_sharing(app, &state).await?;
+        return wait_for_mesh_inference(model_id).await;
+    }
+
     let target = match resolve_mesh_bootstrap_target(&state, model_id).await {
         Ok(Some(target)) => target,
         Ok(None) => {
@@ -768,15 +883,9 @@ pub(crate) async fn ensure_relay_mesh_for_record(
         }
     };
 
-    // Serve→Client re-arm transition (micspiral review #3, intentional-by-design):
-    // if the dead ingress belonged to a *serve* node with running consumer
-    // agents, this re-arms it as a Client (`MeshNodeMode::Client`). That is the
-    // correct/safe recovery here — config-backed serve restoration is
-    // `restore_mesh_sharing`'s job (`MeshNodeMode::Serve`), and
-    // `ensure_client_node_for_model` reuses any live runtime of *either* mode
-    // (the router resolves per-request), so it only cold-starts a Client when
-    // there is genuinely no runtime. Falling back to Client if a serve node
-    // crashed under local pressure is a desirable fail-safe, not a regression.
+    // No serving configuration exists, so this is a genuine consumer-only
+    // start. A configured serving machine is restored above and never reaches
+    // this client fallback.
     ensure_client_node_for_model(&state, model_id, Some(target.endpoint_addr)).await?;
     wait_for_mesh_inference(model_id).await
 }
@@ -792,14 +901,17 @@ pub async fn mesh_stop_node(
     // 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 (taken, bound_relay_url) = {
         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()
+        let bound_relay_url = guard
+            .as_ref()
+            .and_then(|runtime| runtime.start_request().relay_url.clone());
+        (guard.take(), bound_relay_url)
     };
     if let Some(runtime) = taken {
         runtime.stop().await.map_err(|error| error.to_string())?;
@@ -808,11 +920,13 @@ pub async fn mesh_stop_node(
         &app,
         &MeshSharingConfig {
             enabled: false,
+            start_on_next_launch: false,
             model_id: String::new(),
             max_vram_gb: None,
+            relay_url: None,
         },
     )?;
-    mesh_llm::publish_stopped_status_once(&app, "stop").await;
+    mesh_llm::publish_stopped_status_once_at(&app, bound_relay_url.as_deref(), "stop").await;
     Ok(mesh_llm::stopped_status())
 }
 
diff --git a/desktop/src-tauri/src/commands/mesh_llm_tests.rs b/desktop/src-tauri/src/commands/mesh_llm_tests.rs
index ccc5287d62..26eb1f5fba 100644
--- a/desktop/src-tauri/src/commands/mesh_llm_tests.rs
+++ b/desktop/src-tauri/src/commands/mesh_llm_tests.rs
@@ -110,6 +110,74 @@ fn buzz_mesh_name_is_stable_and_does_not_expose_the_relay() {
     assert!(!first.contains("example"));
 }
 
+#[test]
+fn sharing_config_keeps_the_community_where_sharing_was_enabled() {
+    let request = mesh_llm::StartMeshNodeRequest {
+        mode: mesh_llm::MeshNodeMode::Serve,
+        model_id: Some("test-model".to_string()),
+        max_vram_gb: Some(24),
+        join_token: None,
+        mesh_name: Some("buzz-community-test".to_string()),
+        relay_url: Some("wss://community.example".to_string()),
+        trusted_owner_ids: Some(Vec::new()),
+    };
+
+    let config = sharing_config_from_request(&request).expect("valid sharing config");
+    assert_eq!(config.relay_url.as_deref(), Some("wss://community.example"));
+}
+
+#[test]
+fn legacy_sharing_config_without_community_binding_still_loads() {
+    let config: MeshSharingConfig = serde_json::from_value(serde_json::json!({
+        "enabled": true,
+        "modelId": "test-model",
+        "maxVramGb": null
+    }))
+    .expect("legacy sharing config");
+
+    assert_eq!(config.relay_url, None);
+    assert!(!config.start_on_next_launch);
+}
+
+#[test]
+fn new_start_checkpoint_prevents_incomplete_download_restore() {
+    let config = MeshSharingConfig {
+        enabled: true,
+        start_on_next_launch: false,
+        model_id: "test-model".to_string(),
+        max_vram_gb: Some(24),
+        relay_url: Some("wss://community.example".to_string()),
+    };
+
+    let checkpoint = pending_new_start_checkpoint(&config);
+    assert!(!checkpoint.enabled);
+    assert!(!checkpoint.start_on_next_launch);
+    assert_eq!(checkpoint.model_id, config.model_id);
+    assert_eq!(checkpoint.max_vram_gb, config.max_vram_gb);
+    assert_eq!(checkpoint.relay_url, config.relay_url);
+}
+
+#[test]
+fn role_switch_checkpoint_starts_exactly_once_after_restart() {
+    let config = MeshSharingConfig {
+        enabled: true,
+        start_on_next_launch: false,
+        model_id: "test-model".to_string(),
+        max_vram_gb: Some(24),
+        relay_url: Some("wss://community.example".to_string()),
+    };
+
+    let restart = one_shot_restart_checkpoint(&config);
+    assert!(!restart.enabled);
+    assert!(restart.start_on_next_launch);
+
+    let consumed = pending_new_start_checkpoint(&restart);
+    assert!(!consumed.enabled);
+    assert!(!consumed.start_on_next_launch);
+    assert_eq!(consumed.model_id, config.model_id);
+    assert_eq!(consumed.relay_url, config.relay_url);
+}
+
 #[test]
 fn readiness_failure_is_catalog_sync_when_model_never_visible() {
     assert_eq!(
@@ -345,6 +413,7 @@ fn ensure_serve_runtime_serves_other_model() {
                         max_vram_gb: None,
                         join_token: None,
                         mesh_name: None,
+                        relay_url: None,
                         trusted_owner_ids: None,
                     })
                     .await
diff --git a/desktop/src-tauri/src/managed_agents/relay_mesh.rs b/desktop/src-tauri/src/managed_agents/relay_mesh.rs
index 7a6f5b094a..4da889e667 100644
--- a/desktop/src-tauri/src/managed_agents/relay_mesh.rs
+++ b/desktop/src-tauri/src/managed_agents/relay_mesh.rs
@@ -42,15 +42,66 @@ pub fn apply_relay_mesh_env(
         RELAY_MESH_PREFER_MESH_FOR_AUTO_ENV.to_string(),
         "1".to_string(),
     );
-    // Keep the requested response inside smaller local-model context windows,
-    // and spend that budget on an answer/tool call instead of hidden reasoning.
-    // Without both settings Qwen3 either fails the router's fit check at the
-    // agent default (32K) or can consume a tight cap before serializing a tool.
+    // Shared compute runs small local models, which reliably finish a
+    // multi-step turn by writing the answer as prose instead of calling
+    // `send_message` — the reply is then never published. Opt this preset into
+    // the harness fallback that delivers that text. Mesh-only: cloud models
+    // publish their own replies, where the fallback would double-post.
+    // Value is `true`, not `1`: these are clap bool flags, which reject `1`.
     env.insert(
-        "BUZZ_AGENT_MAX_OUTPUT_TOKENS".to_string(),
-        "4096".to_string(),
+        "BUZZ_ACP_DELIVER_PLAIN_REPLIES".to_string(),
+        "true".to_string(),
     );
-    env.insert("BUZZ_AGENT_THINKING_EFFORT".to_string(), "none".to_string());
+    // Offer the typed `send_message` reply tool here too. Small local models
+    // deliver replies far more reliably with it than by composing a
+    // `buzz messages send` shell command; scoped to this preset because adding
+    // a tool changes the toolset — and so the prompt — that every agent sees.
+    env.insert("BUZZ_ACP_SEND_MESSAGE_TOOL".to_string(), "true".to_string());
+    // Keep the requested response inside smaller local-model context windows.
+    // These are defaults, not policy: the effective agent/persona/global env
+    // may deliberately choose a smaller cap or a different effort. This function
+    // runs after those layers during readiness, so never clobber their values.
+    insert_default_if_unset(env, "BUZZ_AGENT_MAX_OUTPUT_TOKENS", "4096");
+    // 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
+    // value is model-specific. Measured with the real prompt and toolset:
+    // gemma-4-E4B delivers 0/8 at `none` but 6/6 with the field absent, while
+    // Qwen3-8B delivers 8/8 either way and burns ~4x the output tokens once
+    // thinking is on (121 -> ~470), risking the 4096 cap. Omitting the field
+    // lets every model use its own default; explicit agent/persona/global
+    // values still apply.
+}
+
+#[cfg(feature = "mesh-llm")]
+fn insert_default_if_unset(
+    env: &mut std::collections::BTreeMap<String, String>,
+    key: &str,
+    value: &str,
+) {
+    if env.get(key).is_none_or(|current| current.trim().is_empty()) {
+        env.insert(key.to_string(), value.to_string());
+    }
+}
+
+/// Build the final Mesh-specific process overrides from the already-resolved
+/// harness environment. Only user-owned generation controls are seeded: the
+/// derived provider/base URL/model values remain authoritative, and unrelated
+/// credentials (notably `OPENAI_API_KEY`) must not be copied back after the
+/// spawn path removes them.
+#[cfg(feature = "mesh-llm")]
+pub fn relay_mesh_process_env(
+    effective_env: &std::collections::BTreeMap<String, String>,
+    model: &str,
+) -> std::collections::BTreeMap<String, String> {
+    let mut env = std::collections::BTreeMap::new();
+    for key in ["BUZZ_AGENT_MAX_OUTPUT_TOKENS", "BUZZ_AGENT_THINKING_EFFORT"] {
+        if let Some(value) = effective_env.get(key) {
+            env.insert(key.to_string(), value.clone());
+        }
+    }
+    apply_relay_mesh_env(&mut env, Some(RELAY_MESH_PROVIDER_ID), Some(model));
+    env
 }
 
 #[cfg(all(test, feature = "mesh-llm"))]
@@ -60,7 +111,7 @@ mod tests {
     use super::*;
 
     #[test]
-    fn native_provider_uses_context_safe_non_reasoning_budget() {
+    fn native_provider_uses_context_safe_tool_calling_budget() {
         let mut env = BTreeMap::new();
         apply_relay_mesh_env(
             &mut env,
@@ -72,14 +123,89 @@ mod tests {
             env.get("BUZZ_AGENT_MAX_OUTPUT_TOKENS").map(String::as_str),
             Some("4096")
         );
-        assert_eq!(
-            env.get("BUZZ_AGENT_THINKING_EFFORT").map(String::as_str),
-            Some("none")
-        );
+        // Must stay unset: any value we pick overrides the model's own chat
+        // template default, and the right value is model-specific ("none"
+        // stops gemma tool-calling; enabling thinking makes Qwen3 burn ~4x the
+        // output budget).
+        assert_eq!(env.get("BUZZ_AGENT_THINKING_EFFORT"), None);
         assert_eq!(
             env.get(RELAY_MESH_PREFER_MESH_FOR_AUTO_ENV)
                 .map(String::as_str),
             Some("1")
         );
+        // Small local models end multi-step turns with prose instead of a
+        // `send_message` call, so shared compute opts into harness delivery.
+        assert_eq!(
+            env.get("BUZZ_ACP_DELIVER_PLAIN_REPLIES")
+                .map(String::as_str),
+            // `true`, not `1`: clap bool flags reject `1` and the harness
+            // exits at startup with "invalid value '1'".
+            Some("true")
+        );
+        // Typed reply tool: measured 3/8 -> 8/8 delivery on gemma-4-E4B.
+        assert_eq!(
+            env.get("BUZZ_ACP_SEND_MESSAGE_TOOL").map(String::as_str),
+            Some("true")
+        );
+    }
+
+    #[test]
+    fn non_mesh_provider_does_not_opt_into_plain_reply_delivery() {
+        let mut env = BTreeMap::new();
+        apply_relay_mesh_env(&mut env, Some("anthropic"), Some("claude-sonnet-4"));
+
+        // Cloud models publish their own replies; delivering streamed text
+        // there would double-post.
+        assert_eq!(env.get("BUZZ_ACP_DELIVER_PLAIN_REPLIES"), None);
+        // And their toolset must stay exactly as it was before this PR.
+        assert_eq!(env.get("BUZZ_ACP_SEND_MESSAGE_TOOL"), None);
+    }
+
+    #[test]
+    fn native_provider_preserves_explicit_generation_controls() {
+        let mut env = BTreeMap::from([
+            (
+                "BUZZ_AGENT_MAX_OUTPUT_TOKENS".to_string(),
+                "2048".to_string(),
+            ),
+            ("BUZZ_AGENT_THINKING_EFFORT".to_string(), "high".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_MAX_OUTPUT_TOKENS").map(String::as_str),
+            Some("2048")
+        );
+        assert_eq!(
+            env.get("BUZZ_AGENT_THINKING_EFFORT").map(String::as_str),
+            Some("high")
+        );
+    }
+
+    #[test]
+    fn process_env_seeds_controls_without_restoring_unrelated_credentials() {
+        let effective_env = BTreeMap::from([
+            (
+                "BUZZ_AGENT_MAX_OUTPUT_TOKENS".to_string(),
+                "1024".to_string(),
+            ),
+            ("OPENAI_API_KEY".to_string(), "must-not-leak".to_string()),
+        ]);
+
+        let env = relay_mesh_process_env(&effective_env, "Gemma-4");
+
+        assert_eq!(
+            env.get("BUZZ_AGENT_MAX_OUTPUT_TOKENS").map(String::as_str),
+            Some("1024")
+        );
+        assert_eq!(
+            env.get("OPENAI_COMPAT_MODEL").map(String::as_str),
+            Some("Gemma-4")
+        );
+        assert!(!env.contains_key("OPENAI_API_KEY"));
     }
 }
diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs
index f3b4cb67fd..37927961ed 100644
--- a/desktop/src-tauri/src/managed_agents/runtime.rs
+++ b/desktop/src-tauri/src/managed_agents/runtime.rs
@@ -869,12 +869,7 @@ pub fn spawn_agent_child(
     // uses the same trim semantics as the preflight callers.
     #[cfg(feature = "mesh-llm")]
     if let Some(ref mesh_model_id) = mesh_model_id {
-        let mut mesh_env = std::collections::BTreeMap::new();
-        super::apply_relay_mesh_env(
-            &mut mesh_env,
-            Some(super::RELAY_MESH_PROVIDER_ID),
-            Some(mesh_model_id.as_str()),
-        );
+        let mesh_env = super::relay_mesh_process_env(&descriptor.env, mesh_model_id);
         command.env_remove("OPENAI_API_KEY");
         for (key, value) in mesh_env {
             command.env(key, value);
diff --git a/desktop/src-tauri/src/mesh_llm/catalog.rs b/desktop/src-tauri/src/mesh_llm/catalog.rs
index 385971cb86..1a11fcfcd1 100644
--- a/desktop/src-tauri/src/mesh_llm/catalog.rs
+++ b/desktop/src-tauri/src/mesh_llm/catalog.rs
@@ -19,12 +19,14 @@ use mesh_llm_system::vram::{format_rated_capacity, rated_capacity_gb};
 /// 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 = "gemma-4-26B-A4B-it-UD-Q4_K_M";
+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";
-const CURATED_SMALL: &str = "Gemma-4-E4B-it-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;
@@ -37,6 +39,16 @@ fn buzz_recommended_model(rated_gb: Option<u64>) -> &'static str {
     }
 }
 
+/// Convert Buzz's pre-0.74 curated package aliases into the canonical model
+/// ids advertised and accepted by Mesh's OpenAI ingress.
+pub(crate) fn canonical_curated_model_id(model_id: &str) -> &str {
+    match model_id.trim() {
+        CURATED_SMALL_ALIAS => CURATED_SMALL,
+        CURATED_LARGE_ALIAS => CURATED_LARGE,
+        other => other,
+    }
+}
+
 /// How a model sits inside this machine's usable AI memory.
 /// Mirrors mesh-llm's private `fit_code_for_size_label` thresholds.
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
@@ -146,12 +158,13 @@ fn build_catalog(
         .filter(|m| !is_draft_only(&m.name))
         .map(|m| {
             let size_gb = parse_size_gb(&m.size);
+            let name = canonical_curated_model_id(&m.name).to_string();
             MeshCatalogEntry {
                 fit: fit_code(size_gb, vram_gb),
-                installed: is_installed(&m.file, &m.name),
+                installed: is_installed(&m.file, &name) || is_installed(&m.file, &m.name),
                 recommended: false,
                 curated: false,
-                name: m.name.clone(),
+                name,
                 size: m.size.clone(),
                 size_gb,
                 description: m.description.clone(),
@@ -166,7 +179,8 @@ fn build_catalog(
         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),
+            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(),
@@ -256,6 +270,8 @@ mod tests {
 
     #[test]
     fn recommendation_follows_buzz_curated_tiers() {
+        assert_eq!(CURATED_SMALL, "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M");
+        assert_eq!(CURATED_LARGE, "unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M");
         // 64GB+ rated machines get the large curated pick.
         let large = build_catalog(None, 64_000_000_000, 64.0, &[]);
         assert_eq!(large.recommended.as_deref(), Some(CURATED_LARGE));
@@ -269,6 +285,22 @@ mod tests {
         assert_eq!(tiny.recommended.as_deref(), Some(CURATED_SMALL));
     }
 
+    #[test]
+    fn curated_package_aliases_migrate_to_openai_model_ids() {
+        assert_eq!(
+            canonical_curated_model_id(CURATED_SMALL_ALIAS),
+            CURATED_SMALL
+        );
+        assert_eq!(
+            canonical_curated_model_id(CURATED_LARGE_ALIAS),
+            CURATED_LARGE
+        );
+        assert_eq!(
+            canonical_curated_model_id("other/model:Q4"),
+            "other/model:Q4"
+        );
+    }
+
     #[test]
     fn curated_picks_lead_the_catalog() {
         let catalog = build_catalog(None, 96_000_000_000, 96.0, &[]);
diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs
index 1e279353b6..066fa46373 100644
--- a/desktop/src-tauri/src/mesh_llm/coordinator.rs
+++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs
@@ -132,7 +132,7 @@ pub async fn start_coordinator(app: AppHandle) {
 /// MeshLLM establishes the encrypted peer transport itself.
 async fn reconcile_buzz_mesh_join(app: &AppHandle) -> Result<(), String> {
     let state = app.state::<AppState>();
-    let peer_ids = {
+    let (peer_ids, relay_url) = {
         let runtime = state.mesh_llm_runtime.lock().await;
         let Some(runtime) = runtime.as_ref() else {
             return Ok(());
@@ -141,10 +141,16 @@ async fn reconcile_buzz_mesh_join(app: &AppHandle) -> Result<(), String> {
             .status_report_payload()
             .await
             .map_err(|error| error.to_string())?;
-        visible_peer_ids(&payload)
+        let relay_url = runtime
+            .start_request()
+            .relay_url
+            .clone()
+            .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state));
+        (visible_peer_ids(&payload), relay_url)
     };
 
-    let targets = crate::commands::mesh_llm::resolve_buzz_mesh_join_targets(&state).await?;
+    let targets =
+        crate::commands::mesh_llm::resolve_buzz_mesh_join_targets_at(&state, &relay_url).await?;
     let Some(target) = targets
         .into_iter()
         .find(|target| !target_is_visible(target, &peer_ids))
@@ -201,8 +207,13 @@ fn target_is_visible(target: &crate::mesh_llm::MeshServeTarget, peer_ids: &[Stri
 enum RosterReconcileAction {
     /// Keep the running allowlist untouched (no-op, or a failure we ride out).
     Keep,
-    /// Restart the node with a freshly resolved roster.
-    Restart(Vec<String>),
+    /// Restart Buzz so MeshLLM is rebuilt with a freshly resolved roster.
+    ///
+    /// MeshLLM's native listeners are process-owned in practice: stopping and
+    /// starting the embedded runtime in one process can terminate Buzz or race
+    /// ports 9337/3131. The process boundary is therefore part of the safety
+    /// contract, not an implementation detail.
+    RestartProcess,
     /// Observed a *shrink* (or empty) once. Hold the current allowlist and
     /// require the same reduced roster on the next poll before tearing down,
     /// so a single transient short-read never drops a member mid-inference.
@@ -224,9 +235,9 @@ fn roster_shrinks(current: &[String], fresh: &[String]) -> bool {
 /// Rules:
 /// - query failed (`Err`)              → `Keep` (never de-admit on a relay blip)
 /// - resolved roster == current        → `Keep` (no-op)
-/// - grows (only additions)            → `Restart` immediately (fast admission)
+/// - grows (only additions)            → `RestartProcess` immediately (fast admission)
 /// - shrinks/empties, first observation → `AwaitConfirm` (hold, re-check next poll)
-/// - shrinks/empties, confirmed         → `Restart` (same reduced roster twice)
+/// - shrinks/empties, confirmed         → `RestartProcess` (same reduced roster twice)
 fn roster_reconcile_action(
     current_owners: &[String],
     pending_shrink: Option<&[String]>,
@@ -248,13 +259,13 @@ fn roster_reconcile_action(
 
     // Growth (pure additions) is safe to apply immediately.
     if !roster_shrinks(current_owners, &fresh) {
-        return RosterReconcileAction::Restart(fresh);
+        return RosterReconcileAction::RestartProcess;
     }
 
     // A shrink (including down to empty) must be confirmed across two
     // consecutive polls with the *same* reduced roster before we tear down.
     match pending_shrink {
-        Some(pending) if pending == fresh => RosterReconcileAction::Restart(fresh),
+        Some(pending) if pending == fresh => RosterReconcileAction::RestartProcess,
         _ => RosterReconcileAction::AwaitConfirm(fresh),
     }
 }
@@ -283,8 +294,13 @@ async fn reconcile_roster(
     // other member on a transient relay blip (the flapping restart loop). Keep
     // the current allowlist and try again on the next poll. A shrink is held
     // for one extra poll (hysteresis) so a single short-read never tears down.
-    let query = crate::commands::mesh_llm::resolve_trusted_owner_ids(&state).await;
-    let fresh = match roster_reconcile_action(current_owners, pending_shrink.as_deref(), query) {
+    let relay_url = current_request
+        .relay_url
+        .as_deref()
+        .map(str::to_owned)
+        .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(&state));
+    let query = crate::commands::mesh_llm::resolve_trusted_owner_ids_at(&state, &relay_url).await;
+    match roster_reconcile_action(current_owners, pending_shrink.as_deref(), query) {
         RosterReconcileAction::Keep => {
             *pending_shrink = None;
             return Ok(());
@@ -294,34 +310,12 @@ async fn reconcile_roster(
             *pending_shrink = Some(reduced);
             return Ok(());
         }
-        RosterReconcileAction::Restart(fresh) => {
+        RosterReconcileAction::RestartProcess => {
             *pending_shrink = None;
-            fresh
         }
-    };
+    }
 
-    let mut request = current_request.clone();
-    request.trusted_owner_ids = Some(fresh);
-    // Bootstrap endpoints are live device state, not configuration. The
-    // endpoint used at the previous start may belong to the member that just
-    // left or to a device whose iroh identity rotated while offline. Resolve a
-    // fresh validated peer for this restart; starting isolated is safe because
-    // the join watcher will converge it when a member next publishes.
-    request.join_token = match crate::commands::mesh_llm::resolve_buzz_mesh_join_targets(&state)
-        .await
-    {
-        Ok(targets) => targets
-            .into_iter()
-            .next()
-            .map(|target| target.endpoint_addr),
-        Err(error) => {
-            eprintln!(
-                "buzz-mesh: could not refresh bootstrap endpoint for roster restart; starting isolated: {error}"
-            );
-            None
-        }
-    };
-    let mut guard = state.mesh_llm_runtime.lock().await;
+    let guard = state.mesh_llm_runtime.lock().await;
     let startup_pending = match guard.as_ref() {
         Some(runtime) => runtime.is_starting().await,
         None => false,
@@ -342,24 +336,14 @@ async fn reconcile_roster(
         // snapshot.
         return Ok(());
     }
-    let Some(running) = guard.take() else {
+    if guard.is_none() {
         return Ok(());
-    };
-    eprintln!("buzz-mesh: membership roster changed; restarting mesh node with fresh allowlist");
-    if let Err(error) = running.stop().await {
-        drop(guard);
-        eprintln!(
-            "buzz-mesh: stopping mesh node for roster restart failed; restarting Buzz instead of racing the occupied ingress: {error}"
-        );
-        app.request_restart();
-        return Err(format!(
-            "mesh node shutdown failed during roster change: {error}"
-        ));
     }
-    let replacement = crate::mesh_llm::DesktopMeshRuntime::start(request)
-        .await
-        .map_err(|error| format!("mesh node restart after roster change failed: {error:#}"))?;
-    *guard = Some(replacement);
+    drop(guard);
+    eprintln!(
+        "buzz-mesh: membership roster changed; restarting Buzz to rebuild MeshLLM with the fresh community allowlist"
+    );
+    app.request_restart();
     Ok(())
 }
 
@@ -377,11 +361,15 @@ pub(crate) async fn publish_current_status_once(app: &AppHandle, reason: &str) {
     }
 }
 
-pub(crate) async fn publish_stopped_status_once(app: &AppHandle, reason: &str) {
+pub(crate) async fn publish_stopped_status_once_at(
+    app: &AppHandle,
+    relay_url: Option<&str>,
+    reason: &str,
+) {
     let state = app.state::<AppState>();
     match tokio::time::timeout(
         STATUS_PUBLISH_TIMEOUT,
-        publish_stopped_status_for_state(&state),
+        publish_stopped_status_for_state(&state, relay_url),
     )
     .await
     {
@@ -396,26 +384,43 @@ pub(crate) async fn publish_stopped_status_once(app: &AppHandle, reason: &str) {
 async fn publish_current_status_for_state(state: &AppState) -> Result<(), String> {
     let identity = super::ensure_owner_identity()
         .map_err(|error| format!("failed to load mesh owner identity: {error}"))?;
-    let mut payload = {
+    let (mut payload, relay_url) = {
         let runtime = state.mesh_llm_runtime.lock().await;
         match runtime.as_ref() {
-            Some(runtime) => runtime
-                .status_report_payload()
-                .await
-                .map_err(|error| error.to_string())?,
-            None => stopped_status_payload(&identity),
+            Some(runtime) => {
+                let payload = runtime
+                    .status_report_payload()
+                    .await
+                    .map_err(|error| error.to_string())?;
+                let relay_url = runtime
+                    .start_request()
+                    .relay_url
+                    .clone()
+                    .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(state));
+                (payload, relay_url)
+            }
+            None => (
+                stopped_status_payload(&identity),
+                crate::relay::relay_ws_url_with_override(state),
+            ),
         }
     };
     bind_payload_to_member(state, &identity, &mut payload)?;
-    publish_status_report(state, payload).await
+    publish_status_report_at(state, &relay_url, payload).await
 }
 
-async fn publish_stopped_status_for_state(state: &AppState) -> Result<(), String> {
+async fn publish_stopped_status_for_state(
+    state: &AppState,
+    relay_url: Option<&str>,
+) -> Result<(), String> {
     let identity = super::ensure_owner_identity()
         .map_err(|error| format!("failed to load mesh owner identity: {error}"))?;
     let mut payload = stopped_status_payload(&identity);
     bind_payload_to_member(state, &identity, &mut payload)?;
-    publish_status_report(state, payload).await
+    let relay_url = relay_url
+        .map(str::to_owned)
+        .unwrap_or_else(|| crate::relay::relay_ws_url_with_override(state));
+    publish_status_report_at(state, &relay_url, payload).await
 }
 
 fn stopped_status_payload(identity: &super::identity::OwnerIdentity) -> serde_json::Value {
@@ -469,13 +474,21 @@ pub(crate) fn build_status_report_event(
     .tags([d, k]))
 }
 
-pub(crate) async fn publish_status_report(
+async fn publish_status_report_at(
     state: &AppState,
+    relay_url: &str,
     payload: serde_json::Value,
 ) -> Result<(), String> {
-    crate::relay::submit_event(build_status_report_event(payload)?, state)
-        .await
-        .map(|_| ())
+    let api_base_url = crate::relay::relay_http_base_url(relay_url);
+    let keys = state.signing_keys()?;
+    crate::relay::submit_event_at_with_keys(
+        build_status_report_event(payload)?,
+        state,
+        &api_base_url,
+        &keys,
+    )
+    .await
+    .map(|_| ())
 }
 
 #[cfg(test)]
@@ -554,11 +567,11 @@ mod tests {
 
     // Growth (pure additions) applies immediately — fast admission is fine.
     #[test]
-    fn roster_growth_restarts_immediately() {
+    fn roster_growth_requests_process_restart_immediately() {
         let current = vec!["owner-a".to_string()];
         let fresh = vec!["owner-a".to_string(), "owner-c".to_string()];
-        let action = roster_reconcile_action(&current, None, Ok(fresh.clone()));
-        assert_eq!(action, RosterReconcileAction::Restart(fresh));
+        let action = roster_reconcile_action(&current, None, Ok(fresh));
+        assert_eq!(action, RosterReconcileAction::RestartProcess);
     }
 
     // A shrink is NOT applied on first observation — it must be confirmed.
@@ -572,11 +585,11 @@ mod tests {
 
     // The same reduced roster on two consecutive polls confirms the shrink.
     #[test]
-    fn roster_shrink_restarts_once_confirmed() {
+    fn roster_shrink_requests_process_restart_once_confirmed() {
         let current = vec!["owner-a".to_string(), "owner-b".to_string()];
         let reduced = vec!["owner-a".to_string()];
         let action = roster_reconcile_action(&current, Some(&reduced), Ok(reduced.clone()));
-        assert_eq!(action, RosterReconcileAction::Restart(reduced));
+        assert_eq!(action, RosterReconcileAction::RestartProcess);
     }
 
     // A shrink that changes between polls is not confirmed — it re-holds with
@@ -600,7 +613,7 @@ mod tests {
         assert_eq!(first, RosterReconcileAction::AwaitConfirm(Vec::new()));
         let empty: Vec<String> = Vec::new();
         let confirmed = roster_reconcile_action(&current, Some(&empty), Ok(Vec::new()));
-        assert_eq!(confirmed, RosterReconcileAction::Restart(Vec::new()));
+        assert_eq!(confirmed, RosterReconcileAction::RestartProcess);
     }
 
     // A shrink followed by recovery to the full roster cancels the teardown.
diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs
index 6e3ab4b28b..e206c53886 100644
--- a/desktop/src-tauri/src/mesh_llm/mod.rs
+++ b/desktop/src-tauri/src/mesh_llm/mod.rs
@@ -1,7 +1,7 @@
 use std::collections::BTreeMap;
 
 mod coordinator;
-pub(crate) use coordinator::{publish_current_status_once, publish_stopped_status_once};
+pub(crate) use coordinator::{publish_current_status_once, publish_stopped_status_once_at};
 pub use coordinator::{start_coordinator, MeshCoordinator, KIND_BUZZ_MESH_MEMBER_STATUS};
 
 mod discovery;
@@ -14,6 +14,7 @@ pub(crate) use discovery::{
 use discovery::{device_name_from_status, endpoint_id_from_status, enrich_status_payload_identity};
 
 mod catalog;
+pub(crate) use catalog::canonical_curated_model_id;
 pub use catalog::{model_catalog, MeshModelCatalog};
 
 mod identity;
@@ -200,6 +201,11 @@ pub struct StartMeshNodeRequest {
     /// accepted from the frontend and contains no relay address.
     #[serde(default, skip_deserializing)]
     pub mesh_name: Option<String>,
+    /// Relay this runtime's community membership and discovery are bound to.
+    /// Injected by the backend when sharing starts and retained across UI
+    /// workspace switches; moving a share requires an explicit stop/start.
+    #[serde(default, skip_deserializing)]
+    pub relay_url: Option<String>,
     /// Mesh owner ids admitted to this node (the member roster from
     /// member-signed discovery notes). `None` = caller did not resolve a roster
     /// (tests, direct invocations): the node runs without allowlist
@@ -308,17 +314,20 @@ pub const MESH_WORKER_STACK_SIZE: usize = 8 * 1024 * 1024;
 /// before the node starts. Without this the download happens *inside*
 /// `serve::start()` where the UI can only show a frozen "starting…" state.
 /// Already-installed models return immediately from the cache scan.
-async fn ensure_model_downloaded(model: &str) -> anyhow::Result<()> {
-    let model_owned = model.to_string();
-    let installed = tokio::task::spawn_blocking(move || {
+async fn model_is_installed(model: &str) -> bool {
+    let model_owned = model.replace("@main", "");
+    tokio::task::spawn_blocking(move || {
         let cache = mesh_llm_node::models::default_huggingface_cache_dir();
         mesh_llm_node::models::scan_installed_models(cache)
             .iter()
-            .any(|m| m.model_ref.contains(&model_owned))
+            .any(|m| m.model_ref.replace("@main", "").contains(&model_owned))
     })
     .await
-    .unwrap_or(false);
-    if installed {
+    .unwrap_or(false)
+}
+
+async fn ensure_model_downloaded(model: &str) -> anyhow::Result<()> {
+    if model_is_installed(model).await {
         return Ok(());
     }
     mesh_llm_host_runtime::models::download_model_ref_with_progress_details(model, true)
diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs
index 0b726c264f..557cd040fa 100644
--- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs
+++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs
@@ -12,6 +12,7 @@ fn pending_client_runtime(
         max_vram_gb: None,
         join_token: Some("initial-token".to_string()),
         mesh_name: None,
+        relay_url: None,
         trusted_owner_ids: None,
     };
     super::DesktopMeshRuntime {
diff --git a/desktop/src-tauri/src/mesh_llm/recovery.rs b/desktop/src-tauri/src/mesh_llm/recovery.rs
index 809fab8993..89ca6396e9 100644
--- a/desktop/src-tauri/src/mesh_llm/recovery.rs
+++ b/desktop/src-tauri/src/mesh_llm/recovery.rs
@@ -153,6 +153,13 @@ fn should_evict_after_probe(
         || consecutive >= DEAD_PROBE_EVICT_THRESHOLD
 }
 
+fn requires_process_restart(
+    mode: crate::mesh_llm::MeshNodeMode,
+    startup_in_progress: bool,
+) -> bool {
+    startup_in_progress || mode == crate::mesh_llm::MeshNodeMode::Serve
+}
+
 /// Probe and, when justified, remove one stale runtime. A closed port is
 /// decisive for a foreground agent start; watchdog and ambiguous/unhealthy
 /// ports require consecutive failures to avoid restarting on a transient load
@@ -161,22 +168,23 @@ pub(crate) async fn recover_stale_mesh_runtime(
     state: &AppState,
     urgency: MeshRecoveryUrgency,
 ) -> MeshRuntimeRecovery {
-    let (candidate_id, startup_in_progress) = match state.mesh_llm_runtime.lock().await.as_ref() {
-        Some(runtime) => (runtime.id(), runtime.is_starting().await),
-        None => {
-            state.mesh_recovery.reset_probe_streak();
-            // A cancelled SDK startup can outlive its Buzz-side task briefly
-            // because the embedded runtime runs on its own thread. Never start
-            // a replacement merely because the tracked handle is gone: first
-            // prove the old ingress is either still useful or has released the
-            // port. This closes the port-conflict loop in #2304.
-            return match probe_mesh_ingress().await {
-                MeshIngressProbe::Live => MeshRuntimeRecovery::Live,
-                MeshIngressProbe::PortClosed => MeshRuntimeRecovery::Absent,
-                MeshIngressProbe::Unhealthy => MeshRuntimeRecovery::ReleasePending,
-            };
-        }
-    };
+    let (candidate_id, startup_in_progress, candidate_mode) =
+        match state.mesh_llm_runtime.lock().await.as_ref() {
+            Some(runtime) => (runtime.id(), runtime.is_starting().await, runtime.mode()),
+            None => {
+                state.mesh_recovery.reset_probe_streak();
+                // A cancelled SDK startup can outlive its Buzz-side task briefly
+                // because the embedded runtime runs on its own thread. Never start
+                // a replacement merely because the tracked handle is gone: first
+                // prove the old ingress is either still useful or has released the
+                // port. This closes the port-conflict loop in #2304.
+                return match probe_mesh_ingress().await {
+                    MeshIngressProbe::Live => MeshRuntimeRecovery::Live,
+                    MeshIngressProbe::PortClosed => MeshRuntimeRecovery::Absent,
+                    MeshIngressProbe::Unhealthy => MeshRuntimeRecovery::ReleasePending,
+                };
+            }
+        };
     let probe = probe_mesh_ingress().await;
     if probe == MeshIngressProbe::Live {
         state.mesh_recovery.reset_probe_streak();
@@ -196,12 +204,13 @@ pub(crate) async fn recover_stale_mesh_runtime(
         return MeshRuntimeRecovery::Debouncing;
     }
 
-    // The pinned SDK does not yield its control handle until the management
-    // API is ready. Dropping its still-pending start future would detach the
-    // embedded runtime thread without sending a shutdown request, so Buzz must
-    // not evict it and race a replacement onto the same ports. A controlled
-    // app relaunch is the only process-owned cleanup boundary in this state.
-    if startup_in_progress {
+    // Never replace a serving runtime in-process. Its native listeners and
+    // model host are process-owned; stopping it here and then cold-starting a
+    // client silently disables Share Compute and can race ports 9337/3131.
+    // Pending client startups have the same ownership problem because the SDK
+    // has not yielded a shutdown handle yet. In both cases, process restart is
+    // the only boundary that preserves the configured role safely.
+    if requires_process_restart(candidate_mode, startup_in_progress) {
         state.mesh_recovery.reset_probe_streak();
         return MeshRuntimeRecovery::RestartRequired;
     }
@@ -241,6 +250,12 @@ pub(crate) async fn recover_stale_mesh_runtime(
 pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Result<(), String> {
     let state = app.state::<AppState>();
     let _rearm_guard = state.mesh_recovery.rearm_lock.lock().await;
+    let runtime_mode = state
+        .mesh_llm_runtime
+        .lock()
+        .await
+        .as_ref()
+        .map(|runtime| runtime.mode());
     let recovery = recover_stale_mesh_runtime(&state, MeshRecoveryUrgency::Watchdog).await;
     let active_pubkeys = active_managed_agent_pubkeys(&state);
     // Mesh participation is resolved through the same definition-authoritative
@@ -254,6 +269,13 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu
         | MeshRuntimeRecovery::Debouncing
         | MeshRuntimeRecovery::Replaced => return Ok(()),
         MeshRuntimeRecovery::RestartRequired => {
+            if runtime_mode == Some(crate::mesh_llm::MeshNodeMode::Serve) {
+                eprintln!(
+                    "buzz-mesh: serving ingress failed; restarting Buzz to restore Share Compute without changing roles"
+                );
+                app.request_restart();
+                return Ok(());
+            }
             let records = crate::managed_agents::load_managed_agents(app).unwrap_or_default();
             if !records.iter().any(|record| {
                 running_relay_mesh_model_id(record, &active_pubkeys, &personas, &global).is_some()
@@ -410,6 +432,7 @@ mod tests {
             name_pool: Vec::new(),
             is_builtin: false,
             is_active: true,
+            shared: false,
             source_team: None,
             source_team_persona_slug: None,
             catalog_source: None,
@@ -481,6 +504,22 @@ mod tests {
         assert!(STALE_STOP_TIMEOUT <= Duration::from_secs(15));
     }
 
+    #[test]
+    fn failed_serving_runtime_requires_process_restart_instead_of_client_fallback() {
+        assert!(requires_process_restart(
+            crate::mesh_llm::MeshNodeMode::Serve,
+            false
+        ));
+        assert!(requires_process_restart(
+            crate::mesh_llm::MeshNodeMode::Client,
+            true
+        ));
+        assert!(!requires_process_restart(
+            crate::mesh_llm::MeshNodeMode::Client,
+            false
+        ));
+    }
+
     #[test]
     fn only_running_relay_mesh_agents_trigger_rearm() {
         let personas: Vec<crate::managed_agents::AgentDefinition> = Vec::new();
diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx
index 76f86f8054..fd7550eff0 100644
--- a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx
+++ b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx
@@ -107,7 +107,9 @@ export function MeshComputeSettingsCard() {
 
   // One-shot hardware-aware catalog fetch. Purely additive: when it fails
   // (stub build, survey error) the card falls back to the free-text field.
-  // Keep an empty draft empty so the UI can explicitly ask the member to choose.
+  // When there is no saved choice, make the curated recommendation the actual
+  // default so a new member can turn Share Compute on directly. An explicit
+  // saved draft always wins.
   React.useEffect(() => {
     let cancelled = false;
     (async () => {
@@ -115,6 +117,11 @@ export function MeshComputeSettingsCard() {
         const value = await meshModelCatalog();
         if (cancelled) return;
         setCatalog(value);
+        setModelInput((current) => {
+          if (current.trim() !== "" || !value.recommended) return current;
+          writeDraft(MODEL_DRAFT_STORAGE_KEY, value.recommended);
+          return value.recommended;
+        });
       } catch {
         // Non-fatal — picker just doesn't render.
       }
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 4cfc553df1..e9ed438086 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -2888,9 +2888,7 @@ const mockMeshState: {
   servingUsage: MockServingUsage;
 } = {
   admitted: true,
-  models: [
-    { id: "hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M", name: "SmolLM2 135M" },
-  ],
+  models: [{ id: "Gemma-4-E4B-it-Q4_K_M", name: "Gemma 4 E4B" }],
   denyReason: "not a relay member",
   nodeState: "off",
   nodeMode: null,
@@ -2899,9 +2897,7 @@ const mockMeshState: {
 
 function resetMockMesh() {
   mockMeshState.admitted = true;
-  mockMeshState.models = [
-    { id: "hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M", name: "SmolLM2 135M" },
-  ];
+  mockMeshState.models = [{ id: "Gemma-4-E4B-it-Q4_K_M", name: "Gemma 4 E4B" }];
   mockMeshState.denyReason = "not a relay member";
   mockMeshState.nodeState = "off";
   mockMeshState.nodeMode = null;
@@ -9737,6 +9733,25 @@ export function maybeInstallE2eTauriMocks() {
       }
       case "mesh_installed_models":
         return mockMeshState.models;
+      case "mesh_model_catalog":
+        return {
+          gpuName: "Mock Apple GPU",
+          vramDisplay: "32 GB",
+          vramGb: 32,
+          recommended: "Gemma-4-E4B-it-Q4_K_M",
+          entries: [
+            {
+              name: "Gemma-4-E4B-it-Q4_K_M",
+              size: "3.5GB",
+              sizeGb: 3.5,
+              description: "Buzz-curated local agent model",
+              fit: "comfortable",
+              installed: true,
+              recommended: true,
+              curated: true,
+            },
+          ],
+        };
       case "mesh_node_status":
         return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
       case "mesh_serving_usage":
diff --git a/desktop/tests/e2e/mesh-compute.spec.ts b/desktop/tests/e2e/mesh-compute.spec.ts
index 7c360368a2..b2e8fc28a9 100644
--- a/desktop/tests/e2e/mesh-compute.spec.ts
+++ b/desktop/tests/e2e/mesh-compute.spec.ts
@@ -15,7 +15,7 @@ type E2eWindow = Window & {
   }) => void;
 };
 
-test("Share compute has a clear empty state and starts and stops sharing", async ({
+test("Share compute selects the curated default and starts and stops sharing", async ({
   page,
 }) => {
   await installMockBridge(page);
@@ -30,22 +30,32 @@ test("Share compute has a clear empty state and starts and stops sharing", async
   await expect(card).toContainText(
     "Choose a suggested model below, or enter a model reference or local file",
   );
-  await expect(toggle).toBeDisabled();
-
-  await model.fill("hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M");
+  await expect(model).toHaveValue("Gemma-4-E4B-it-Q4_K_M");
+  await expect(toggle).toBeEnabled();
   await expect(card).toContainText(
     "Buzz downloads remote models when sharing starts",
   );
-  await expect(toggle).toBeEnabled();
 
   await toggle.click();
   await expect(toggle).toBeChecked();
-  await expect(card).toContainText("Sharing SmolLM2 135M with relay members");
+  await expect(card).toContainText("Sharing Gemma 4 E4B with relay members");
   await expect
     .poll(() =>
       page.evaluate(() => (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []),
     )
     .toContain("mesh_start_node");
+  await expect
+    .poll(() =>
+      page.evaluate(
+        () => (window as E2eWindow).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [],
+      ),
+    )
+    .toContainEqual({
+      command: "mesh_start_node",
+      payload: {
+        request: { mode: "serve", modelId: "Gemma-4-E4B-it-Q4_K_M" },
+      },
+    });
 
   await toggle.click();
   await expect(toggle).not.toBeChecked();
