notedeck

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

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(&note, "seq").and_then(|s| s.parse::<u32>().ok());
     44         let source_data = get_tag_value(&note, "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 }