diff --git a/core/archipelago/src/mesh/flash.rs b/core/archipelago/src/mesh/flash.rs index 607f9f7a..35d850dc 100644 --- a/core/archipelago/src/mesh/flash.rs +++ b/core/archipelago/src/mesh/flash.rs @@ -427,13 +427,29 @@ pub async fn start_flash_job( let mut svc = bg_service.write().await; if let Some(s) = svc.as_mut() { - if let Ok(config) = super::load_config(&data_dir).await { - if let Err(e) = s.configure(config).await { - warn!(error = %e, "Failed to resume mesh listener after flash"); + match super::load_config(&data_dir).await { + Ok(config) => { + // Only resume if mesh is actually still enabled per the + // CURRENT persisted config — confirmed live 2026-07-23: + // unconditionally forcing a restart here, regardless of + // `enabled`, overrode a user's own concurrent "disable + // mesh" toggle and left the listener running while + // config said disabled. That inconsistent state is what + // made a later legitimate "Keep As Is" click (which + // correctly tries to start on a false→true transition) + // fail with "already running" — the listener had already + // been force-started behind the config's back. + let should_run = config.enabled; + if let Err(e) = s.configure(config).await { + warn!(error = %e, "Failed to resume mesh listener after flash"); + } + if should_run { + if let Err(e) = s.start() { + warn!(error = %e, "Failed to restart mesh listener after flash"); + } + } } - } - if let Err(e) = s.start() { - warn!(error = %e, "Failed to restart mesh listener after flash"); + Err(e) => warn!(error = %e, "Failed to load mesh config after flash"), } } }); diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index b2e071a2..fd007b79 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -735,10 +735,18 @@ impl MeshService { self.server_name = name; } - /// Start the background mesh listener. + /// Start the background mesh listener. Idempotent: if the listener is + /// already running, this is a harmless no-op rather than an error — + /// confirmed live 2026-07-23, a real race between the flash job's own + /// post-flash restart and a concurrent user "Keep As Is" click (both + /// legitimately trying to ensure the listener is running) surfaced this + /// as a user-facing "Mesh listener already running" RPC error. Ensuring + /// the listener is running is the intent every caller actually has; + /// whichever caller's start() happens to win the race, the other + /// finding it already satisfied is success, not failure. pub fn start(&mut self) -> Result<()> { if self.listener_handle.is_some() { - anyhow::bail!("Mesh listener already running"); + return Ok(()); } let (shutdown_tx, shutdown_rx) = watch::channel(false);