notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 7490143289a082fd35b3fd13f0199c8b5153e907
parent 94a7f9c4b40b07b11b07518802a27ef87aa232c1
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 18 Feb 2026 10:33:57 -0800

fix remote session status using stale replaceable event revisions

When multiple revisions of a kind-31988 session state event arrive
out of order (e.g. after relay reconnect from sleep), the last one
processed would win regardless of timestamp. Track remote_status_ts
so we only apply updates from newer events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 23++++++++++++++++++-----
Mcrates/notedeck_dave/src/session.rs | 4++++
Mcrates/notedeck_dave/src/session_loader.rs | 2++
3 files changed, 24 insertions(+), 5 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -1321,6 +1321,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr agentic.seen_note_ids = loaded.note_ids; // Set remote status from state event agentic.remote_status = AgentStatus::from_status_str(&state.status); + agentic.remote_status_ts = state.created_at; // Set up live conversation subscription for remote sessions if is_remote && agentic.live_conversation_sub.is_none() { @@ -1395,14 +1396,18 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Skip deleted sessions entirely — don't create or keep them if status_str == "deleted" { - // If we have this session locally, remove it + // If we have this session locally, remove it (only if this + // event is newer than the last state we applied). if existing_ids.contains(claude_sid) { + let ts = note.created_at(); let to_delete: Vec<SessionId> = self .session_manager .iter() .filter(|s| { - s.agentic.as_ref().and_then(|a| a.event_session_id()) - == Some(claude_sid) + s.agentic.as_ref().is_some_and(|a| { + a.event_session_id() == Some(claude_sid) + && ts > a.remote_status_ts + }) }) .map(|s| s.id) .collect(); @@ -1419,14 +1424,21 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr continue; } - // Update remote_status for existing remote sessions + // Update remote_status for existing remote sessions, but only + // if this event is newer than the one we already applied. + // Multiple revisions of the same replaceable event can arrive + // out of order (e.g. after a relay reconnect). if existing_ids.contains(claude_sid) { + let ts = note.created_at(); let new_status = AgentStatus::from_status_str(status_str); for session in self.session_manager.iter_mut() { if session.is_remote() { if let Some(agentic) = &mut session.agentic { - if agentic.event_session_id() == Some(claude_sid) { + if agentic.event_session_id() == Some(claude_sid) + && ts > agentic.remote_status_ts + { agentic.remote_status = new_status; + agentic.remote_status_ts = ts; } } } @@ -1488,6 +1500,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr agentic.seen_note_ids = loaded.note_ids; // Set remote status agentic.remote_status = AgentStatus::from_status_str(status_str); + agentic.remote_status_ts = note.created_at(); // Set up live conversation subscription for remote sessions if is_remote && agentic.live_conversation_sub.is_none() { diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -115,6 +115,9 @@ pub struct AgenticSessionData { /// Status as reported by the remote desktop's kind-31988 event. /// Only meaningful when session source is Remote. pub remote_status: Option<AgentStatus>, + /// Timestamp of the kind-31988 event that last set `remote_status`. + /// Used to ignore older replaceable event revisions that arrive out of order. + pub remote_status_ts: u64, /// Subscription for live kind-1988 conversation events from relays. /// Used by remote sessions to receive new messages in real-time. pub live_conversation_sub: Option<nostrdb::Subscription>, @@ -151,6 +154,7 @@ impl AgenticSessionData { live_threading: ThreadingState::new(), perm_response_sub: None, remote_status: None, + remote_status_ts: 0, live_conversation_sub: None, seen_note_ids: HashSet::new(), } diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs @@ -229,6 +229,7 @@ pub struct SessionState { pub cwd: String, pub status: String, pub hostname: String, + pub created_at: u64, } /// Load all session states from kind-31988 events in ndb. @@ -275,6 +276,7 @@ pub fn load_session_states(ndb: &Ndb, txn: &Transaction) -> Vec<SessionState> { cwd: get_tag_value(&note, "cwd").unwrap_or("").to_string(), status: get_tag_value(&note, "status").unwrap_or("idle").to_string(), hostname: get_tag_value(&note, "hostname").unwrap_or("").to_string(), + created_at: note.created_at(), }); }