commit 172743f0a6f2f5f039ed176bc77bd4a93ee8087c
parent cb39bcb0e95e30b7b08d3dd4c18e9bbdf26b0cbc
Author: William Casarin <jb55@jb55.com>
Date: Sun, 15 Feb 2026 23:51:23 -0800
session_events: seed live threading from archive events on resume
Live events were starting a new NIP-10 root instead of threading onto
the existing archive conversation. Seed ThreadingState with root/last
note IDs from loaded archive events so resumed sessions thread correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
3 files changed, 55 insertions(+), 7 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -1082,14 +1082,24 @@ impl notedeck::App for Dave {
let notes = ctx.ndb.poll_for_notes(pending.sub, 4096);
if !notes.is_empty() {
let txn = Transaction::new(ctx.ndb).expect("txn");
- let messages = session_loader::load_session_messages(
+ let loaded = session_loader::load_session_messages(
ctx.ndb,
&txn,
&pending.claude_session_id,
);
if let Some(session) = self.session_manager.get_mut(pending.dave_session_id) {
- tracing::info!("loaded {} messages into chat UI", messages.len());
- session.chat = messages;
+ tracing::info!("loaded {} messages into chat UI", loaded.messages.len());
+ session.chat = loaded.messages;
+
+ // Seed live threading from archive events so new events
+ // thread as replies to the existing conversation.
+ if let (Some(root), Some(last)) =
+ (loaded.root_note_id, loaded.last_note_id)
+ {
+ if let Some(agentic) = &mut session.agentic {
+ agentic.live_threading.seed(root, last, loaded.event_count);
+ }
+ }
}
self.pending_message_load = None;
}
diff --git a/crates/notedeck_dave/src/session_events.rs b/crates/notedeck_dave/src/session_events.rs
@@ -105,6 +105,16 @@ impl ThreadingState {
line.timestamp_secs().or(self.last_timestamp)
}
+ /// Seed threading state from existing events (e.g. loaded from ndb).
+ ///
+ /// Sets root and last note IDs so that subsequent live events
+ /// thread correctly as replies to the existing conversation.
+ pub fn seed(&mut self, root_note_id: [u8; 32], last_note_id: [u8; 32], event_count: u32) {
+ self.root_note_id = Some(root_note_id);
+ self.last_note_id = Some(last_note_id);
+ self.seq = event_count;
+ }
+
/// Record a built event's note ID, associated with a JSONL uuid.
fn record(&mut self, uuid: Option<&str>, note_id: [u8; 32]) {
if self.root_note_id.is_none() {
diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs
@@ -9,11 +9,23 @@ use crate::session_events::{get_tag_value, AI_CONVERSATION_KIND};
use crate::Message;
use nostrdb::{Filter, Ndb, Transaction};
+/// Result of loading session messages, including threading info for live events.
+pub struct LoadedSession {
+ pub messages: Vec<Message>,
+ /// Root note ID of the conversation (first event chronologically).
+ pub root_note_id: Option<[u8; 32]>,
+ /// Last note ID of the conversation (most recent event).
+ pub last_note_id: Option<[u8; 32]>,
+ /// Total number of events found.
+ pub event_count: u32,
+}
+
/// Load conversation messages from ndb for a given session ID.
///
/// Returns messages in chronological order, suitable for populating
-/// `ChatSession.chat` before streaming begins.
-pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) -> Vec<Message> {
+/// `ChatSession.chat` before streaming begins. Also returns note IDs
+/// for seeding live threading state.
+pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) -> LoadedSession {
let filter = Filter::new()
.kinds([AI_CONVERSATION_KIND as u64])
.tags([session_id], 'd')
@@ -22,7 +34,14 @@ pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) ->
let results = match ndb.query(txn, &[filter], 10000) {
Ok(r) => r,
- Err(_) => return vec![],
+ Err(_) => {
+ return LoadedSession {
+ messages: vec![],
+ root_note_id: None,
+ last_note_id: None,
+ event_count: 0,
+ }
+ }
};
// Collect notes with their created_at for sorting
@@ -34,6 +53,10 @@ pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) ->
// Sort by created_at (chronological order)
notes.sort_by_key(|note| note.created_at());
+ let event_count = notes.len() as u32;
+ let root_note_id = notes.first().map(|n| *n.id());
+ let last_note_id = notes.last().map(|n| *n.id());
+
let mut messages = Vec::new();
for note in ¬es {
let content = note.content();
@@ -66,7 +89,12 @@ pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) ->
}
}
- messages
+ LoadedSession {
+ messages,
+ root_note_id,
+ last_note_id,
+ event_count,
+ }
}
fn truncate(s: &str, max_chars: usize) -> String {