From 46101f0a5db0d65e73914b5e9fc5f9298bac9bb7 Mon Sep 17 00:00:00 2001 From: iroiro147 Date: Sun, 2 Aug 2026 19:04:04 +0530 Subject: [PATCH] fix(relay): soft-delete workflow definition event on workflow delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buzz workflows delete` removes the `workflows` row and invalidates the engine cache (so triggers fail correctly), but the kind:30620 definition event itself stayed live — `workflows list` and `workflows get` were still reading it via Nostr REQ, so every surface an operator could inspect reported the workflow as live. Triggering afterwards was the only way to find out it was gone. The generic addressable-events arm in `handle_a_tag_deletion` already soft-deletes by coordinate; the workflow arm never did. Extend the workflow arm to call `soft_delete_by_coordinate(30620, pubkey, d_tag)` once the row has been successfully deleted — gated on the delete succeeding so a failed match can't tombstone a live workflow's event. Refs: #2879 Signed-off-by: Sarthak Singh --- .../buzz-relay/src/handlers/side_effects.rs | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/buzz-relay/src/handlers/side_effects.rs b/crates/buzz-relay/src/handlers/side_effects.rs index 660a55fef3..dc7cfbfc59 100644 --- a/crates/buzz-relay/src/handlers/side_effects.rs +++ b/crates/buzz-relay/src/handlers/side_effects.rs @@ -2103,7 +2103,7 @@ async fn handle_a_tag_deletion( } buzz_core::kind::KIND_WORKFLOW_DEF => { // Try UUID first (workflow_id); fall back to name-based lookup. - if let Ok(wf_id) = uuid::Uuid::parse_str(d_tag) { + let workflow_deleted: Option = if let Ok(wf_id) = uuid::Uuid::parse_str(d_tag) { let channel_id = state .db .delete_workflow_for_owner(tenant.community(), wf_id, &actor_bytes) @@ -2115,6 +2115,7 @@ async fn handle_a_tag_deletion( .invalidate_channel_workflows(tenant.community(), channel_id); } tracing::info!(workflow_id = %wf_id, "Workflow deleted via NIP-09 a-tag (UUID)"); + Some(wf_id) } else { // Name-based lookup match state @@ -2136,14 +2137,66 @@ async fn handle_a_tag_deletion( .invalidate_channel_workflows(tenant.community(), channel_id); } tracing::info!(workflow_id = %wf.id, name = d_tag, "Workflow deleted via NIP-09 a-tag (name)"); + Some(wf.id) } Ok(None) => { tracing::warn!( "NIP-09 a-tag deletion: no workflow '{d_tag}' found for owner" ); + None } Err(e) => { tracing::warn!("NIP-09 a-tag deletion: DB lookup failed: {e}"); + None + } + } + }; + + // Soft-delete the kind:30620 definition event so REQs (CLI + // `workflows list` / `workflows get`) stop returning it. Without + // this the workflow row is gone but the definition event stayed + // live and every read path disagreed with triggers — see #2879. + // Only do this when the row delete actually succeeded above, so + // a failed delete (e.g. owner mismatch) can't tombstone a live + // workflow's event out from under it. + if workflow_deleted.is_some() { + let pubkey_bytes = match hex::decode(pubkey_hex) { + Ok(b) => b, + Err(e) => { + tracing::warn!("NIP-09 a-tag deletion: invalid pubkey hex {pubkey_hex}: {e}"); + Vec::new() + } + }; + if !pubkey_bytes.is_empty() { + match state + .db + .soft_delete_by_coordinate( + tenant.community(), + buzz_core::kind::KIND_WORKFLOW_DEF as i32, + &pubkey_bytes, + d_tag, + event.created_at.as_secs() as i64, + ) + .await + { + Ok(true) => { + tracing::info!( + d_tag = d_tag, + "kind:30620 workflow definition event soft-deleted" + ) + } + Ok(false) => { + tracing::debug!( + d_tag = d_tag, + "no live kind:30620 event row matched coordinate" + ) + } + Err(e) => { + tracing::warn!( + d_tag = d_tag, + "soft-delete of kind:30620 event failed: {e}" + ) + } } } }