session_reconstructor.rs (2786B)
1 //! Reconstruct JSONL from kind-1989 source-data nostr events stored in ndb. 2 //! 3 //! Queries events by session ID (`d` tag), sorts by `seq` tag, 4 //! extracts `source-data` tags, and returns the original JSONL lines. 5 6 use crate::session_events::{get_tag_value, AI_SOURCE_DATA_KIND}; 7 use nostrdb::{Filter, Ndb, Transaction}; 8 9 #[derive(Debug)] 10 pub enum ReconstructError { 11 Query(String), 12 Io(String), 13 } 14 15 impl std::fmt::Display for ReconstructError { 16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 match self { 18 ReconstructError::Query(e) => write!(f, "ndb query failed: {}", e), 19 ReconstructError::Io(e) => write!(f, "io error: {}", e), 20 } 21 } 22 } 23 24 /// Reconstruct JSONL lines from ndb events for a given session ID. 25 /// 26 /// Returns lines in original order (sorted by `seq` tag), suitable for 27 /// writing to a JSONL file or feeding to `claude --resume`. 28 pub fn reconstruct_jsonl_lines( 29 ndb: &Ndb, 30 txn: &Transaction, 31 session_id: &str, 32 ) -> Result<Vec<String>, ReconstructError> { 33 let filters = [Filter::new() 34 .kinds([AI_SOURCE_DATA_KIND as u64]) 35 .tags([session_id], 'd') 36 .limit(10000) 37 .build()]; 38 39 // Use ndb.fold to iterate events without collecting QueryResults 40 let mut entries: Vec<(u32, String)> = Vec::new(); 41 42 let _ = ndb.fold(txn, &filters, &mut entries, |entries, note| { 43 let seq = get_tag_value(¬e, "seq").and_then(|s| s.parse::<u32>().ok()); 44 let source_data = get_tag_value(¬e, "source-data"); 45 46 // Only events with source-data contribute JSONL lines. 47 // Split events only have source-data on the first event (i=0), 48 // so we naturally get one JSONL line per original JSONL line. 49 if let (Some(seq), Some(data)) = (seq, source_data) { 50 entries.push((seq, data.to_string())); 51 } 52 53 entries 54 }); 55 56 // Sort by seq for original ordering 57 entries.sort_by_key(|(seq, _)| *seq); 58 59 // Deduplicate by source-data content (safety net for re-ingestion) 60 entries.dedup_by(|a, b| a.1 == b.1); 61 62 Ok(entries.into_iter().map(|(_, data)| data).collect()) 63 } 64 65 /// Reconstruct JSONL and write to a file. 66 /// 67 /// Returns the number of lines written. 68 pub fn reconstruct_jsonl_file( 69 ndb: &Ndb, 70 txn: &Transaction, 71 session_id: &str, 72 output_path: &std::path::Path, 73 ) -> Result<usize, ReconstructError> { 74 let lines = reconstruct_jsonl_lines(ndb, txn, session_id)?; 75 let count = lines.len(); 76 77 use std::io::Write; 78 let mut file = 79 std::fs::File::create(output_path).map_err(|e| ReconstructError::Io(e.to_string()))?; 80 81 for line in &lines { 82 writeln!(file, "{}", line).map_err(|e| ReconstructError::Io(e.to_string()))?; 83 } 84 85 Ok(count) 86 }