From ab56054aeb8a66cc5baf69ae78826f8c0dd29463 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 17 Jun 2026 06:12:56 -0400 Subject: [PATCH] fix(federation): remove-node also purges the mesh contact/thread (#2) federation.remove-node only edited nodes.json, so a removed/renamed node (e.g. a stale "Arch HP") lingered in the mesh chat list with its old thread. Capture the node's pubkey before removal, then purge its synthetic mesh peer, shared secret, messages, presence, and persisted contact entry via the new mesh::purge_federation_peer. Combined with the #42 name refresh, stale federation contacts can now be fully cleaned from a node. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/api/rpc/federation/handlers.rs | 27 ++++++++++++++++ core/archipelago/src/mesh/mod.rs | 32 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/core/archipelago/src/api/rpc/federation/handlers.rs b/core/archipelago/src/api/rpc/federation/handlers.rs index d80690f0..5a63e9bf 100644 --- a/core/archipelago/src/api/rpc/federation/handlers.rs +++ b/core/archipelago/src/api/rpc/federation/handlers.rs @@ -262,9 +262,36 @@ impl RpcHandler { .ok_or_else(|| anyhow::anyhow!("Missing 'did' parameter"))?; validate_did(did)?; + // Capture the node's pubkey before removal so we can also purge its + // synthetic mesh contact/thread (#2) — remove_node only touches + // nodes.json, which would otherwise leave a stale chat contact behind. + let removed_pubkey = federation::load_nodes(&self.config.data_dir) + .await + .ok() + .and_then(|nodes| { + nodes + .into_iter() + .find(|n| n.did == did) + .map(|n| n.pubkey) + }); + let nodes = federation::remove_node(&self.config.data_dir, did).await?; info!(did = %did, "Removed node from federation"); + if let Some(pubkey) = removed_pubkey.filter(|p| !p.is_empty()) { + let svc = self.mesh_service.read().await; + if let Some(svc) = svc.as_ref() { + let contact_id = mesh::federation_peer_contact_id(&pubkey); + mesh::purge_federation_peer( + &svc.shared_state(), + contact_id, + &pubkey, + &self.config.data_dir, + ) + .await; + } + } + Ok(serde_json::json!({ "removed": true, "nodes_remaining": nodes.len(), diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index 618348d1..46984dd3 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -88,6 +88,38 @@ pub(crate) async fn upsert_federation_peer( contact_id } +/// Purge a federation peer from all live mesh state and persisted contacts so +/// removing a node (federation.remove-node) also clears its chat contact, +/// thread, and any per-contact customisation — otherwise a stale/renamed node +/// (e.g. an old "Arch HP" entry) lingers in the chat list even after it's gone +/// from `nodes.json` (#2). Keyed by the synthetic `contact_id` for the peer +/// table/messages and by `pubkey_hex` for the pubkey-keyed contacts/presence +/// stores. +pub(crate) async fn purge_federation_peer( + state: &Arc, + contact_id: u32, + pubkey_hex: &str, + data_dir: &Path, +) { + state.peers.write().await.remove(&contact_id); + state.shared_secrets.write().await.remove(&contact_id); + state + .messages + .write() + .await + .retain(|m| m.peer_contact_id != contact_id); + state.presence.write().await.remove(pubkey_hex); + let mut contacts = state.contacts.write().await; + if contacts.remove(pubkey_hex).is_some() { + let snapshot = contacts.clone(); + drop(contacts); + if let Err(e) = save_mesh_contacts(data_dir, &snapshot).await { + warn!("Failed to persist mesh contacts after purge: {}", e); + } + } + state.update_peer_count().await; +} + /// Load federation nodes from disk and upsert each as a synthetic mesh peer. /// Called at MeshService startup so the chat list already contains every /// known federation node — users can share files to them without first