From 3ca2c4babb95640fd8947a7084566f6ffc674546 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 19 Aug 2026 22:24:38 +0000 Subject: [PATCH] fix(sync): preserve hooks after maintainer pre-save Cold exact-ID reprocessing collapsed exact-duplicate and replaced save statuses into one terminal duplicate. Maintainer admission intentionally pre-saves newly authorized announcements for membership reconciliation, so that early return skipped the local notification and dependency expansion needed to recover state and Git data. Preserve the database distinction in the accepted-event persistence outcome. Exact duplicates reached after the initial absence check continue through accepted-event hooks, while genuinely replaced events still return Duplicate without broadcast fan-out. Apply the same distinction to hot-cache promotion hooks. Correctness assumes an exact duplicate after the pre-admission event-by-ID miss is the intentional policy pre-save or a benign concurrent insertion; Replaced continues to mean a newer coordinate is authoritative. This does not change membership policy, replacement ordering, or fetch scheduling. Validation: nix develop --command cargo test --locked; focused cold invitation and rejected-maintainer Git-sync integration tests; replaced-sync-event unit regression; cargo fmt --check; git diff --check. --- CHANGELOG.md | 7 +++++++ src/nostr/persistence.rs | 13 +++++++++++-- src/purgatory/promotion_hooks.rs | 17 +++++++++++------ src/sync/mod.rs | 9 ++++++--- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4031b87..a4dc4dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -156,6 +156,13 @@ Performance and Security fixes - along with other improvements; immediate upgrad ### Fixed +- Restore cold rejected-maintainer recovery after policy admission pre-saves + the newly authorized announcement for membership reconciliation. An exact + duplicate from that intentional pre-save now still broadcasts the + announcement and expands its state/Git dependencies, while a genuinely + superseded replaceable event remains classified as a duplicate without + downstream fan-out. + - Discover historical repository roots and descendants from the NIP-65 inboxes of accepted repository owners and maintainers. Public Sync+ instances probe exact repository coordinates and every known root on those diff --git a/src/nostr/persistence.rs b/src/nostr/persistence.rs index aa398b9..5831a25 100644 --- a/src/nostr/persistence.rs +++ b/src/nostr/persistence.rs @@ -26,8 +26,14 @@ pub enum SaveContext { pub enum AcceptedEventSaveOutcome { /// The database inserted the event. Saved, - /// The database already had this event or retained a newer replaceable event. + /// The exact event is already stored. + /// + /// Write-policy admission may intentionally pre-save an event when + /// subsequent policy work needs to query it. Callers that established the + /// event was absent before admission must still run their post-save hooks. Duplicate, + /// The database retained a newer replaceable event instead. + Replaced, } /// Nostr-owned facade for accepted-event persistence side effects. @@ -55,9 +61,12 @@ impl<'a> EventPersistence<'a> { let outcome = match status { SaveEventStatus::Success => AcceptedEventSaveOutcome::Saved, - SaveEventStatus::Rejected(RejectedReason::Duplicate | RejectedReason::Replaced) => { + SaveEventStatus::Rejected(RejectedReason::Duplicate) => { AcceptedEventSaveOutcome::Duplicate } + SaveEventStatus::Rejected(RejectedReason::Replaced) => { + AcceptedEventSaveOutcome::Replaced + } SaveEventStatus::Rejected(reason) => { return Err(anyhow::anyhow!( "Database rejected accepted event {}: {reason:?}", diff --git a/src/purgatory/promotion_hooks.rs b/src/purgatory/promotion_hooks.rs index 111ac5c..8bd671a 100644 --- a/src/purgatory/promotion_hooks.rs +++ b/src/purgatory/promotion_hooks.rs @@ -88,15 +88,17 @@ impl NostrPurgatoryPromotionHooks { .save_accepted_event(&state, SaveContext::HotCacheReprocess) .await { - Ok(AcceptedEventSaveOutcome::Saved) => { + Ok( + AcceptedEventSaveOutcome::Saved | AcceptedEventSaveOutcome::Duplicate, + ) => { rejected_events_index.remove(&state.id); relay.notify_event(state.clone()); } - Ok(AcceptedEventSaveOutcome::Duplicate) => { + Ok(AcceptedEventSaveOutcome::Replaced) => { rejected_events_index.remove(&state.id); debug!( event_id = %state.id, - "Re-processed state event was already superseded or stored" + "Re-processed state event was superseded" ); } Err(e) => { @@ -201,7 +203,10 @@ impl PurgatoryPromotionHooks for NostrPurgatoryPromotionHooks { .save_accepted_event(&hot_event, SaveContext::HotCacheReprocess) .await { - Ok(AcceptedEventSaveOutcome::Saved) => { + Ok( + AcceptedEventSaveOutcome::Saved + | AcceptedEventSaveOutcome::Duplicate, + ) => { rejected_events_index.remove(&hot_event.id); relay.notify_event(hot_event.clone()); self.reprocess_state_dependencies( @@ -217,7 +222,7 @@ impl PurgatoryPromotionHooks for NostrPurgatoryPromotionHooks { "Maintainer announcement accepted and saved on re-processing" ); } - Ok(AcceptedEventSaveOutcome::Duplicate) => { + Ok(AcceptedEventSaveOutcome::Replaced) => { rejected_events_index.remove(&hot_event.id); self.reprocess_state_dependencies( write_policy, @@ -229,7 +234,7 @@ impl PurgatoryPromotionHooks for NostrPurgatoryPromotionHooks { .await; debug!( event_id = %hot_event.id, - "Re-processed maintainer announcement was already superseded or stored" + "Re-processed maintainer announcement was superseded" ); } Err(e) => { diff --git a/src/sync/mod.rs b/src/sync/mod.rs index c86b2ac..41cc3d7 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -7568,12 +7568,15 @@ impl SyncManager { WritePolicyResult::Accept => { // Save event to database match write_policy.save_accepted_event(event, save_context).await { - Ok(crate::nostr::persistence::AcceptedEventSaveOutcome::Saved) => {} - Ok(crate::nostr::persistence::AcceptedEventSaveOutcome::Duplicate) => { + Ok( + crate::nostr::persistence::AcceptedEventSaveOutcome::Saved + | crate::nostr::persistence::AcceptedEventSaveOutcome::Duplicate, + ) => {} + Ok(crate::nostr::persistence::AcceptedEventSaveOutcome::Replaced) => { tracing::trace!( event_id = %event.id, relay = %relay_url, - "Database retained an existing event during sync" + "Database retained a newer replaceable event during sync" ); return ProcessResult::Duplicate; }