notedeck

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

commit 7b4430340c1d6f091bcc628b6aa47a36ecabac4d
parent d9c46bc912d7f7d9953a846bc243f53046055a34
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 16:25:04 -0800

dave: persist backend type in nostr session state events

Codex sessions were being restored as Claude after round-tripping
through nostrdb because the backend tag was missing from kind-31988
events. Add a "backend" tag to session state events and parse it
on restore, falling back to Claude for old events without the tag.

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

Diffstat:
Mcrates/notedeck_dave/src/backend/traits.rs | 21+++++++++++++++++++++
Mcrates/notedeck_dave/src/lib.rs | 11++++++++++-
Mcrates/notedeck_dave/src/session_events.rs | 4++++
Mcrates/notedeck_dave/src/session_loader.rs | 3+++
4 files changed, 38 insertions(+), 1 deletion(-)

diff --git a/crates/notedeck_dave/src/backend/traits.rs b/crates/notedeck_dave/src/backend/traits.rs @@ -38,6 +38,27 @@ impl BackendType { BackendType::Remote => "", } } + + /// Stable string for Nostr event tags. + pub fn as_str(&self) -> &'static str { + match self { + BackendType::OpenAI => "openai", + BackendType::Claude => "claude", + BackendType::Codex => "codex", + BackendType::Remote => "remote", + } + } + + /// Parse from a Nostr event tag value. + pub fn from_tag_str(s: &str) -> Option<BackendType> { + match s { + "openai" => Some(BackendType::OpenAI), + "claude" => Some(BackendType::Claude), + "codex" => Some(BackendType::Codex), + "remote" => Some(BackendType::Remote), + _ => None, + } + } } /// Trait for AI backend implementations diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -197,6 +197,7 @@ struct DeletedSessionInfo { title: String, cwd: String, home_dir: String, + backend: BackendType, } /// Subscription waiting for ndb to index 1988 conversation events. @@ -1433,6 +1434,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr status, &self.hostname, &session.details.home_dir, + session.backend_type.as_str(), &sk, ), &format!("publishing session state: {} -> {}", claude_sid, status), @@ -1466,6 +1468,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr "deleted", &self.hostname, &info.home_dir, + info.backend.as_str(), &sk, ), &format!( @@ -1557,13 +1560,18 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr tracing::info!("restoring {} sessions from ndb", states.len()); for state in &states { + let backend = state + .backend + .as_deref() + .and_then(BackendType::from_tag_str) + .unwrap_or(BackendType::Claude); let cwd = std::path::PathBuf::from(&state.cwd); let dave_sid = self.session_manager.new_resumed_session( cwd, state.claude_session_id.clone(), state.title.clone(), AiMode::Agentic, - BackendType::Claude, + backend, ); // Load conversation history from kind-1988 events @@ -2103,6 +2111,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr title: session.details.title.clone(), cwd: agentic.cwd.to_string_lossy().to_string(), home_dir: session.details.home_dir.clone(), + backend: session.backend_type, }); } } diff --git a/crates/notedeck_dave/src/session_events.rs b/crates/notedeck_dave/src/session_events.rs @@ -737,6 +737,7 @@ pub fn build_session_state_event( status: &str, hostname: &str, home_dir: &str, + backend: &str, secret_key: &[u8; 32], ) -> Result<BuiltEvent, EventBuildError> { let mut builder = init_note_builder(AI_SESSION_STATE_KIND, "", Some(now_secs())); @@ -753,6 +754,7 @@ pub fn build_session_state_event( builder = builder.start_tag().tag_str("status").tag_str(status); builder = builder.start_tag().tag_str("hostname").tag_str(hostname); builder = builder.start_tag().tag_str("home_dir").tag_str(home_dir); + builder = builder.start_tag().tag_str("backend").tag_str(backend); // Discoverability builder = builder.start_tag().tag_str("t").tag_str("ai-session-state"); @@ -1259,6 +1261,7 @@ mod tests { "working", "my-laptop", "/home/testuser", + "claude", &sk, ) .unwrap(); @@ -1278,6 +1281,7 @@ mod tests { assert!(json.contains("working")); assert!(json.contains("/tmp/project")); assert!(json.contains(r#""hostname","my-laptop"#)); + assert!(json.contains(r#""backend","claude"#)); } #[test] diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs @@ -251,6 +251,7 @@ pub struct SessionState { pub status: String, pub hostname: String, pub home_dir: String, + pub backend: Option<String>, pub created_at: u64, } @@ -297,6 +298,7 @@ pub fn load_session_states(ndb: &Ndb, txn: &Transaction) -> Vec<SessionState> { status: get_tag_value(&note, "status").unwrap_or("idle").to_string(), hostname: get_tag_value(&note, "hostname").unwrap_or("").to_string(), home_dir: get_tag_value(&note, "home_dir").unwrap_or("").to_string(), + backend: get_tag_value(&note, "backend").map(|s| s.to_string()), created_at: note.created_at(), }); } @@ -342,6 +344,7 @@ pub fn latest_valid_session( status: get_tag_value(note, "status").unwrap_or("idle").to_string(), hostname: get_tag_value(note, "hostname").unwrap_or("").to_string(), home_dir: get_tag_value(note, "home_dir").unwrap_or("").to_string(), + backend: get_tag_value(note, "backend").map(|s| s.to_string()), created_at: note.created_at(), }) }