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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Zaki Manian
2026-03-01 08:53:32 +00:00
committed by GitHub
co-authored by Claude Opus 4.6 Illia Polosukhin gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
parent 7b883a02c0
commit fa52df593d
2 changed files with 79 additions and 0 deletions
+54
View File
@@ -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<String> = 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<String> {
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 {
+25
View File
@@ -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<String> =
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.