commit 03df939b63a085312a9e966aa36ce28eedfd9369
parent 56987d99d9568d219d9b791a0422c619a64a6f43
Author: William Casarin <jb55@jb55.com>
Date: Wed, 18 Feb 2026 10:50:23 -0800
fix query_replaceable_filtered not handling arbitrary fold order
ndb.fold iterates notes in storage order, not chronological. If a
newer deleted revision was visited before an older non-deleted one,
the predicate rejection was a no-op (nothing to remove yet), and the
older revision would then be inserted unchallenged — resurrecting
deleted sessions as zombies.
Fix by always tracking the highest created_at per d-tag regardless of
predicate result, storing Option<NoteKey> so rejected revisions still
block older ones from winning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs
@@ -34,11 +34,14 @@ pub fn query_replaceable_filtered(
filters: &[Filter],
predicate: impl Fn(&nostrdb::Note) -> bool,
) -> Vec<NoteKey> {
- // Fold: for each d-tag value, track (created_at, NoteKey) of the latest
+ // Fold: for each d-tag value, track the latest created_at and optionally
+ // a NoteKey (only if the latest revision passes the predicate).
+ // Notes may arrive in any order from ndb.fold, so we always track the
+ // highest timestamp and only keep a key when that revision is valid.
let best = ndb.fold(
txn,
filters,
- std::collections::HashMap::<String, (u64, NoteKey)>::new(),
+ std::collections::HashMap::<String, (u64, Option<NoteKey>)>::new(),
|mut acc, note| {
let Some(d_tag) = get_tag_value(¬e, "d") else {
return acc;
@@ -52,22 +55,22 @@ pub fn query_replaceable_filtered(
}
}
- if predicate(¬e) {
- acc.insert(
- d_tag.to_string(),
- (created_at, note.key().expect("note key")),
- );
+ let key = if predicate(¬e) {
+ Some(note.key().expect("note key"))
} else {
- // Latest revision rejected — remove any older revision we kept
- acc.remove(d_tag);
- }
+ None
+ };
+ acc.insert(d_tag.to_string(), (created_at, key));
acc
},
);
match best {
- Ok(map) => map.into_values().map(|(_, key)| key).collect(),
+ Ok(map) => map
+ .into_values()
+ .filter_map(|(_, key)| key)
+ .collect(),
Err(_) => vec![],
}
}