From a7e2dc846e8be50926acf62cfcaa2c584a41fb91 Mon Sep 17 00:00:00 2001 From: Mat Balez <60949391+matbalez@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:31:44 -0700 Subject: [PATCH] fix(desktop): keep mesh consumers admitted Co-authored-by: Mat Balez <60949391+matbalez@users.noreply.github.com> Signed-off-by: Mat Balez <60949391+matbalez@users.noreply.github.com> --- desktop/src-tauri/src/commands/workspace.rs | 8 +++++ desktop/src-tauri/src/mesh_llm/coordinator.rs | 35 ++++--------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index c602bdce42..5b5220b2ec 100644 --- a/desktop/src-tauri/src/commands/workspace.rs +++ b/desktop/src-tauri/src/commands/workspace.rs @@ -175,6 +175,14 @@ pub async fn apply_workspace( .await .map_err(|e| format!("spawn_blocking failed: {e}"))??; + // The coordinator starts before React applies the selected workspace, so + // its startup publication may have used the fallback relay and placeholder + // identity. Republish immediately with the now-active relay and member key. + // This also makes a consumer-only MeshLLM owner visible for admission + // without waiting for the next heartbeat. + #[cfg(feature = "mesh-llm")] + crate::mesh_llm::publish_current_status_once(&restore_app, "workspace apply").await; + let state = restore_app.state::(); if state .managed_agent_restore_pending diff --git a/desktop/src-tauri/src/mesh_llm/coordinator.rs b/desktop/src-tauri/src/mesh_llm/coordinator.rs index 8e7b2f9536..d25315a027 100644 --- a/desktop/src-tauri/src/mesh_llm/coordinator.rs +++ b/desktop/src-tauri/src/mesh_llm/coordinator.rs @@ -38,13 +38,14 @@ pub async fn start_coordinator(app: AppHandle) { let publisher_app = app.clone(); let status_publisher = tokio::spawn(async move { - // Clear a stale serving status promptly after an app restart. Once the - // node is stopped, the explicit stopped event is sufficient; only a - // running node needs periodic freshness heartbeats. + // Clear a stale serving status promptly after an app restart. Keep + // publishing while stopped too: serving nodes build their admission + // allowlist from fresh member statuses, so consumer-only identities + // must remain fresh even though they advertise no serving targets. publish_current_status_once(&publisher_app, "startup").await; loop { tokio::time::sleep(STATUS_PUBLISH_INTERVAL).await; - publish_running_status_once(&publisher_app).await; + publish_current_status_once(&publisher_app, "heartbeat").await; } }); let roster_app = app.clone(); @@ -118,13 +119,6 @@ pub(crate) async fn publish_stopped_status_once(app: &AppHandle, reason: &str) { } } -async fn publish_running_status_once(app: &AppHandle) { - let state = app.state::(); - if let Err(error) = publish_running_status_for_state(&state).await { - eprintln!("buzz-mesh: periodic status report failed: {error}"); - } -} - 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}"))?; @@ -150,23 +144,6 @@ async fn publish_stopped_status_for_state(state: &AppState) -> Result<(), String publish_status_report(state, payload).await } -async fn publish_running_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 runtime = state.mesh_llm_runtime.lock().await; - let Some(runtime) = runtime.as_ref() else { - return Ok(()); - }; - runtime - .status_report_payload() - .await - .map_err(|error| error.to_string())? - }; - bind_payload_to_member(state, &identity, &mut payload)?; - publish_status_report(state, payload).await -} - fn stopped_status_payload(identity: &super::identity::OwnerIdentity) -> serde_json::Value { serde_json::json!({ "ownerId": identity.owner_id, @@ -234,7 +211,7 @@ mod tests { use super::*; #[test] - fn heartbeat_leaves_room_before_status_expires() { + fn member_heartbeat_leaves_room_before_admission_status_expires() { assert!( STATUS_PUBLISH_INTERVAL.as_secs() * 2 < super::super::discovery::STATUS_FRESHNESS_SECS );