From 5833c9bdf815699838a0445f750b99b26fd4a3bd Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 8 Jan 2026 00:41:02 +0000 Subject: feat(purgatory): track expired events to prevent infinite re-sync loops Adds expired event tracking to prevent proactive sync from repeatedly fetching and re-adding events that expired from purgatory without finding git data. Key features: - Track expired events for 7 days to prevent re-sync loops - Distinguish synced vs user-submitted events (via socket address) - Allow users to retry expired events (git data might now be available) - Reject synced expired events (prevents infinite loop) - Daily cleanup of expired event records older than 7 days Implementation: - Added expired_events: DashMap to Purgatory - Updated event_ids() to include both purgatory + expired events - Added is_expired(), mark_expired(), cleanup_expired_events() - Updated cleanup() to mark expired events automatically - Added is_synced detection in WritePolicy (localhost:0 = synced) - Policy layer checks is_synced && is_expired() before rejecting Behavior: - Negentropy: Filters expired events before fetching (optimal) - REQ+EOSE: Rejects synced expired events at policy layer - User submissions: Always allowed to retry (skip expired check) Testing: - Added 5 new tests for expired event tracking - All 222 tests passing Fixes the infinite re-sync loop where events without git data would expire, get synced again, expire again, repeat forever. --- src/nostr/policy/state.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'src/nostr/policy/state.rs') diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index 68b1e97..7bbb379 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs @@ -43,8 +43,12 @@ impl StatePolicy { /// Process a state event: validate and align owner repositories /// + /// # Arguments + /// * `event` - The state event to process + /// * `is_synced` - True if this event came from proactive sync (vs user-submitted) + /// /// Returns the true if git data already availale or false if added to purgatory - pub async fn process_state_event(&self, event: &Event) -> Result { + pub async fn process_state_event(&self, event: &Event, is_synced: bool) -> Result { // Parse state to get HEAD and branch info let state = RepositoryState::from_event(event.clone()).context("Failed to parse state event")?; @@ -120,6 +124,20 @@ impl StatePolicy { // Event will be saved and broadcast by relay builder Ok(WritePolicyResult::Accept) } else { + // Only reject expired events if they're from sync (not user-submitted) + // User-submitted events should be allowed to retry in case git data became available + if is_synced && self.ctx.purgatory.is_expired(&event.id) { + tracing::debug!( + event_id = %event.id, + identifier = %state.identifier, + "State event previously expired from purgatory (synced), rejecting to prevent re-sync loop" + ); + return Ok(WritePolicyResult::Reject { + status: false, + message: "invalid: previously expired from purgatory without git data".into(), + }); + } + // If no git data - add to purgatory // (add_state automatically enqueues for background sync) self.ctx -- cgit v1.2.3