From fa52df593d0bd6743bf01020eedb5f13e8638e8c Mon Sep 17 00:00:00 2001 From: Zaki Manian Date: Sun, 1 Mar 2026 00:53:32 -0800 Subject: [PATCH] fix: persist channel activation state across restarts (#432) * fix: persist channel activation state across restarts (#392) Channels activated via the web UI were lost on restart because active_channel_names was only in memory. Now persist activation state to the settings store under "activated_channels" and auto-activate persisted channels on startup. Co-Authored-By: Claude Opus 4.6 * fix: log warnings for channel activation load failures Replace silent catch-all with explicit error logging when database queries or deserialization fails for activated channels. Addresses Gemini review feedback on PR #432. Co-Authored-By: Claude Opus 4.6 * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Illia Polosukhin Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/extensions/manager.rs | 54 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 25 ++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index cdcb459a..44d598cd 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -168,6 +168,53 @@ impl ExtensionManager { active.extend(names); } + /// Persist the set of active channel names to the settings store. + /// + /// Saved under key `activated_channels` so channels auto-activate on restart. + async fn persist_active_channels(&self) { + let Some(ref store) = self.store else { + return; + }; + let names: Vec = self + .active_channel_names + .read() + .await + .iter() + .cloned() + .collect(); + let value = serde_json::json!(names); + if let Err(e) = store + .set_setting(&self.user_id, "activated_channels", &value) + .await + { + tracing::warn!(error = %e, "Failed to persist activated_channels setting"); + } + } + + /// Load previously activated channel names from the settings store. + /// + /// Returns channel names that were activated in a prior session so they can + /// be auto-activated at startup. + pub async fn load_persisted_active_channels(&self) -> Vec { + let Some(ref store) = self.store else { + return Vec::new(); + }; + match store.get_setting(&self.user_id, "activated_channels").await { + Ok(Some(value)) => match serde_json::from_value(value) { + Ok(names) => names, + Err(e) => { + tracing::warn!(error = %e, "Failed to deserialize activated_channels"); + Vec::new() + } + }, + Ok(None) => Vec::new(), + Err(e) => { + tracing::warn!(error = %e, "Failed to load activated_channels setting"); + Vec::new() + } + } + } + /// Set the SSE broadcast sender for pushing extension status events to the web UI. pub async fn set_sse_sender( &self, @@ -530,6 +577,10 @@ impl ExtensionManager { Ok(format!("Removed WASM tool '{}'", name)) } ExtensionKind::WasmChannel => { + // Remove from active set and persist + self.active_channel_names.write().await.remove(name); + self.persist_active_channels().await; + // Delete channel files let wasm_path = self.wasm_channels_dir.join(format!("{}.wasm", name)); let cap_path = self @@ -1993,6 +2044,9 @@ impl ExtensionManager { .await .insert(channel_name.clone()); + // Persist activation state so the channel auto-activates on restart + self.persist_active_channels().await; + tracing::info!(channel = %channel_name, "Hot-activated WASM channel"); Ok(ActivateResult { diff --git a/src/main.rs b/src/main.rs index 55a56b1d..08728307 100644 --- a/src/main.rs +++ b/src/main.rs @@ -609,6 +609,8 @@ async fn async_main() -> anyhow::Result<()> { if let Some(ref ext_mgr) = components.extension_manager && let Some((rt, ps, router)) = wasm_channel_runtime_state.take() { + let active_at_startup: std::collections::HashSet = + loaded_wasm_channel_names.iter().cloned().collect(); ext_mgr.set_active_channels(loaded_wasm_channel_names).await; ext_mgr .set_channel_runtime( @@ -620,6 +622,29 @@ async fn async_main() -> anyhow::Result<()> { ) .await; tracing::info!("Channel runtime wired into extension manager for hot-activation"); + + // Auto-activate channels that were active in a previous session. + let persisted = ext_mgr.load_persisted_active_channels().await; + for name in &persisted { + if !active_at_startup.contains(name) { + match ext_mgr.activate(name).await { + Ok(result) => { + tracing::info!( + channel = %name, + message = %result.message, + "Auto-activated persisted channel" + ); + } + Err(e) => { + tracing::warn!( + channel = %name, + error = %e, + "Failed to auto-activate persisted channel" + ); + } + } + } + } } // Wire SSE sender into extension manager for broadcasting status events.