@@ -80,6 +80,16 @@ pub struct OpenClawMessage {
8080 pub created_at : Option < chrono:: DateTime < chrono:: Utc > > ,
8181}
8282
83+ /// Open an OpenClaw SQLite database file via libsql for read-only access.
84+ #[ cfg( feature = "import" ) ]
85+ async fn open_sqlite ( db_path : & Path ) -> Result < libsql:: Connection , ImportError > {
86+ let db = libsql:: Builder :: new_local ( db_path)
87+ . build ( )
88+ . await
89+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
90+ db. connect ( ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) )
91+ }
92+
8393/// Reader for OpenClaw data files and databases.
8494pub struct OpenClawReader {
8595 openclaw_dir : PathBuf ,
@@ -226,115 +236,123 @@ impl OpenClawReader {
226236
227237 /// Read all memory chunks from an OpenClaw SQLite database.
228238 #[ cfg( feature = "import" ) ]
229- pub fn read_memory_chunks (
239+ pub async fn read_memory_chunks (
230240 & self ,
231241 db_path : & Path ,
232242 ) -> Result < Vec < OpenClawMemoryChunk > , ImportError > {
233- use rusqlite:: Connection ;
234-
235- let conn = Connection :: open ( db_path) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
236-
237- let mut stmt = conn
238- . prepare ( "SELECT path, content, embedding, chunk_index FROM chunks" )
239- . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
243+ let conn = open_sqlite ( db_path) . await ?;
240244
241- let chunks = stmt
242- . query_map ( [ ] , |row| {
243- let path: String = row. get ( 0 ) ?;
244- let content: String = row. get ( 1 ) ?;
245- let embedding_bytes: Option < Vec < u8 > > = row. get ( 2 ) ?;
246- let chunk_index: i32 = row. get ( 3 ) ?;
247-
248- // Convert binary embedding blob to Vec<f32> if present
249- let embedding = embedding_bytes. map ( |bytes| {
250- bytes
251- . chunks ( 4 )
252- . map ( |chunk| {
253- if chunk. len ( ) == 4 {
254- f32:: from_le_bytes ( [ chunk[ 0 ] , chunk[ 1 ] , chunk[ 2 ] , chunk[ 3 ] ] )
255- } else {
256- 0.0
257- }
258- } )
259- . collect ( )
260- } ) ;
261-
262- Ok ( OpenClawMemoryChunk {
263- path,
264- content,
265- embedding,
266- chunk_index,
267- } )
268- } )
245+ let mut rows = conn
246+ . query (
247+ "SELECT path, content, embedding, chunk_index FROM chunks" ,
248+ ( ) ,
249+ )
250+ . await
269251 . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
270252
271253 let mut result = Vec :: new ( ) ;
272- for chunk_result in chunks {
273- result. push ( chunk_result. map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?) ;
254+ while let Some ( row) = rows
255+ . next ( )
256+ . await
257+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?
258+ {
259+ let path: String = row. get ( 0 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
260+ let content: String = row. get ( 1 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
261+ let embedding_blob: Option < Vec < u8 > > =
262+ row. get ( 2 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
263+ let chunk_index: i32 = row. get ( 3 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
264+
265+ // Convert binary embedding blob to Vec<f32> if present
266+ let embedding = embedding_blob. map ( |bytes| {
267+ bytes
268+ . chunks ( 4 )
269+ . map ( |chunk| {
270+ if chunk. len ( ) == 4 {
271+ f32:: from_le_bytes ( [ chunk[ 0 ] , chunk[ 1 ] , chunk[ 2 ] , chunk[ 3 ] ] )
272+ } else {
273+ 0.0
274+ }
275+ } )
276+ . collect ( )
277+ } ) ;
278+
279+ result. push ( OpenClawMemoryChunk {
280+ path,
281+ content,
282+ embedding,
283+ chunk_index,
284+ } ) ;
274285 }
275286
276287 Ok ( result)
277288 }
278289
279290 /// Read all conversations from an OpenClaw SQLite database.
280291 #[ cfg( feature = "import" ) ]
281- pub fn read_conversations (
292+ pub async fn read_conversations (
282293 & self ,
283294 db_path : & Path ,
284295 ) -> Result < Vec < OpenClawConversation > , ImportError > {
285- use rusqlite :: Connection ;
296+ let conn = open_sqlite ( db_path ) . await ? ;
286297
287- let conn = Connection :: open ( db_path) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
288-
289- // First, read all conversations
290- let mut conv_stmt = conn
291- . prepare ( "SELECT id, channel, created_at FROM conversations ORDER BY created_at DESC" )
298+ let mut conv_rows = conn
299+ . query (
300+ "SELECT id, channel, created_at FROM conversations ORDER BY created_at DESC" ,
301+ ( ) ,
302+ )
303+ . await
292304 . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
293305
294306 let mut conversations = Vec :: new ( ) ;
295- let conv_rows = conv_stmt
296- . query_map ( [ ] , |row| {
297- let id: String = row. get ( 0 ) ?;
298- let channel: String = row. get ( 1 ) ?;
299- let created_at: Option < String > = row. get ( 2 ) ?;
300-
301- let created_at = created_at
302- . and_then ( |s| chrono:: DateTime :: parse_from_rfc3339 ( & s) . ok ( ) )
303- . map ( |dt| dt. with_timezone ( & chrono:: Utc ) ) ;
304-
305- Ok ( ( id, channel, created_at) )
306- } )
307- . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
307+ while let Some ( row) = conv_rows
308+ . next ( )
309+ . await
310+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?
311+ {
312+ let id: String = row. get ( 0 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
313+ let channel: String = row. get ( 1 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
314+ let created_at: Option < String > =
315+ row. get ( 2 ) . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
308316
309- for row_result in conv_rows {
310- let ( id , channel , created_at ) =
311- row_result . map_err ( |e| ImportError :: Sqlite ( e . to_string ( ) ) ) ? ;
317+ let created_at = created_at
318+ . and_then ( |s| chrono :: DateTime :: parse_from_rfc3339 ( & s ) . ok ( ) )
319+ . map ( |dt| dt . with_timezone ( & chrono :: Utc ) ) ;
312320
313321 // Read messages for this conversation
314- let mut msg_stmt = conn. prepare (
315- "SELECT role, content, created_at FROM messages WHERE conversation_id = ? ORDER BY created_at"
316- )
317- . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
318-
319- let messages = msg_stmt
320- . query_map ( [ & id] , |row| {
321- let role: String = row. get ( 0 ) ?;
322- let content: String = row. get ( 1 ) ?;
323- let created_at: Option < String > = row. get ( 2 ) ?;
324-
325- let created_at = created_at
326- . and_then ( |s| chrono:: DateTime :: parse_from_rfc3339 ( & s) . ok ( ) )
327- . map ( |dt| dt. with_timezone ( & chrono:: Utc ) ) ;
322+ let mut msg_rows = conn
323+ . query (
324+ "SELECT role, content, created_at FROM messages WHERE conversation_id = ?1 ORDER BY created_at" ,
325+ libsql:: params![ id. as_str( ) ] ,
326+ )
327+ . await
328+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
328329
329- Ok ( OpenClawMessage {
330- role,
331- content,
332- created_at,
333- } )
334- } )
330+ let mut messages = Vec :: new ( ) ;
331+ while let Some ( msg_row) = msg_rows
332+ . next ( )
333+ . await
335334 . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?
336- . collect :: < Result < Vec < _ > , _ > > ( )
337- . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
335+ {
336+ let role: String = msg_row
337+ . get ( 0 )
338+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
339+ let content: String = msg_row
340+ . get ( 1 )
341+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
342+ let msg_created_at: Option < String > = msg_row
343+ . get ( 2 )
344+ . map_err ( |e| ImportError :: Sqlite ( e. to_string ( ) ) ) ?;
345+
346+ let msg_created_at = msg_created_at
347+ . and_then ( |s| chrono:: DateTime :: parse_from_rfc3339 ( & s) . ok ( ) )
348+ . map ( |dt| dt. with_timezone ( & chrono:: Utc ) ) ;
349+
350+ messages. push ( OpenClawMessage {
351+ role,
352+ content,
353+ created_at : msg_created_at,
354+ } ) ;
355+ }
338356
339357 conversations. push ( OpenClawConversation {
340358 id,
0 commit comments