commit 8d0b9e02d3a2f1f7f37a1414a201d0da24589dfb
parent 8a9243bdcc00ea26293e25bd038d4e14bca6842a
Author: William Casarin <jb55@jb55.com>
Date: Tue, 17 Feb 2026 14:47:27 -0800
fix duplicate messages on phone and enable phone-to-desktop messaging
Add note IDs to seen_note_ids in ingest_live_event so events echoed
back from the relay are skipped. Subscribe local sessions to
conversation events and handle incoming user messages from remote
clients, dispatching them to the backend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
1 file changed, 67 insertions(+), 17 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -215,6 +215,8 @@ fn ingest_live_event(
secret_key,
) {
Ok(event) => {
+ // Mark as seen so we don't double-process when it echoes back from the relay
+ agentic.seen_note_ids.insert(event.note_id);
pns_ingest(ndb, &event.note_json, secret_key);
Some(event)
}
@@ -553,11 +555,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
info.tools.len(),
info.agents.len()
);
- // Set up permission response subscription when we learn
- // the claude session ID (used as `d` tag for filtering)
+ // Set up subscriptions when we learn the claude session ID
if let Some(agentic) = &mut session.agentic {
- if agentic.perm_response_sub.is_none() {
- if let Some(ref csid) = info.claude_session_id {
+ if let Some(ref csid) = info.claude_session_id {
+ // Permission response subscription (filtered to ai-permission tag)
+ if agentic.perm_response_sub.is_none() {
let filter = nostrdb::Filter::new()
.kinds([session_events::AI_CONVERSATION_KIND as u64])
.tags([csid.as_str()], 'd')
@@ -579,6 +581,28 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
}
}
+ // Conversation subscription for incoming remote user messages
+ if agentic.live_conversation_sub.is_none() {
+ let filter = nostrdb::Filter::new()
+ .kinds([session_events::AI_CONVERSATION_KIND as u64])
+ .tags([csid.as_str()], 'd')
+ .build();
+ match app_ctx.ndb.subscribe(&[filter]) {
+ Ok(sub) => {
+ tracing::info!(
+ "subscribed for conversation events (session {})",
+ csid
+ );
+ agentic.live_conversation_sub = Some(sub);
+ }
+ Err(e) => {
+ tracing::warn!(
+ "failed to subscribe for conversation events: {:?}",
+ e
+ );
+ }
+ }
+ }
}
agentic.session_info = Some(info);
}
@@ -1458,21 +1482,22 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
///
/// For local sessions: only process `role=user` messages arriving from
/// remote clients (phone), collecting them for backend dispatch.
- fn poll_remote_conversation_events(&mut self, ndb: &nostrdb::Ndb) {
+ fn poll_remote_conversation_events(
+ &mut self,
+ ndb: &nostrdb::Ndb,
+ ) -> Vec<(SessionId, String)> {
+ let mut remote_user_messages: Vec<(SessionId, String)> = Vec::new();
let session_ids = self.session_manager.session_ids();
for session_id in session_ids {
let Some(session) = self.session_manager.get_mut(session_id) else {
continue;
};
- // Only remote sessions need to poll for conversation events
- if !session.is_remote() {
- continue;
- }
- let Some(agentic) = &mut session.agentic else {
- continue;
- };
- let Some(sub) = agentic.live_conversation_sub else {
- continue;
+ let is_remote = session.is_remote();
+
+ // Get sub without holding agentic borrow
+ let sub = match session.agentic.as_ref().and_then(|a| a.live_conversation_sub) {
+ Some(s) => s,
+ None => continue,
};
let note_keys = ndb.poll_for_notes(sub, 128);
@@ -1495,13 +1520,33 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
for note in ¬es {
// Skip events we've already processed (dedup)
let note_id = *note.id();
- if !agentic.seen_note_ids.insert(note_id) {
+ let dominated = session
+ .agentic
+ .as_mut()
+ .map(|a| !a.seen_note_ids.insert(note_id))
+ .unwrap_or(true);
+ if dominated {
continue;
}
let content = note.content();
let role = session_events::get_tag_value(note, "role");
+ // Local sessions: only process incoming user messages from remote clients
+ if !is_remote {
+ if role == Some("user") {
+ tracing::info!("received remote user message for local session");
+ session.chat.push(Message::User(content.to_string()));
+ session.update_title_from_last_message();
+ remote_user_messages.push((session_id, content.to_string()));
+ }
+ continue;
+ }
+
+ let Some(agentic) = &mut session.agentic else {
+ continue;
+ };
+
match role {
Some("user") => {
session.chat.push(Message::User(content.to_string()));
@@ -1591,6 +1636,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
}
}
+ remote_user_messages
}
/// Delete a session and clean up backend resources
@@ -1926,8 +1972,12 @@ impl notedeck::App for Dave {
// Poll for new session states from PNS-unwrapped relay events
self.poll_session_state_events(ctx);
- // Poll for live conversation events on remote sessions
- self.poll_remote_conversation_events(ctx.ndb);
+ // Poll for live conversation events on all sessions.
+ // Returns user messages from remote clients that need backend dispatch.
+ let remote_user_msgs = self.poll_remote_conversation_events(ctx.ndb);
+ for (sid, _msg) in remote_user_msgs {
+ self.send_user_message_for(sid, ctx, ui.ctx());
+ }
// Process pending archive conversion (JSONL → nostr events)
if let Some((file_path, dave_sid, claude_sid)) = self.pending_archive_convert.take() {