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 {