@@ -145,6 +145,17 @@ pub struct SummaryBatchJobRow {
145145 pub finished_at : Option < String > ,
146146}
147147
148+ /// Lifecycle cleanup progress/status snapshot.
149+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
150+ pub struct LifecycleCleanupJobRow {
151+ pub status : String ,
152+ pub deleted_sessions : u32 ,
153+ pub deleted_summaries : u32 ,
154+ pub message : Option < String > ,
155+ pub started_at : Option < String > ,
156+ pub finished_at : Option < String > ,
157+ }
158+
148159fn infer_tool_from_source_path ( source_path : Option < & str > ) -> Option < & ' static str > {
149160 let source_path = source_path. map ( |path| path. to_ascii_lowercase ( ) ) ?;
150161
@@ -1436,6 +1447,53 @@ impl LocalDb {
14361447 Ok ( row)
14371448 }
14381449
1450+ pub fn set_lifecycle_cleanup_job ( & self , payload : & LifecycleCleanupJobRow ) -> Result < ( ) > {
1451+ self . conn ( ) . execute (
1452+ "INSERT INTO lifecycle_cleanup_jobs \
1453+ (id, status, deleted_sessions, deleted_summaries, message, started_at, finished_at, updated_at) \
1454+ VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, datetime('now')) \
1455+ ON CONFLICT(id) DO UPDATE SET \
1456+ status=excluded.status, \
1457+ deleted_sessions=excluded.deleted_sessions, \
1458+ deleted_summaries=excluded.deleted_summaries, \
1459+ message=excluded.message, \
1460+ started_at=excluded.started_at, \
1461+ finished_at=excluded.finished_at, \
1462+ updated_at=datetime('now')",
1463+ params ! [
1464+ payload. status,
1465+ payload. deleted_sessions as i64 ,
1466+ payload. deleted_summaries as i64 ,
1467+ payload. message,
1468+ payload. started_at,
1469+ payload. finished_at,
1470+ ] ,
1471+ ) ?;
1472+ Ok ( ( ) )
1473+ }
1474+
1475+ pub fn get_lifecycle_cleanup_job ( & self ) -> Result < Option < LifecycleCleanupJobRow > > {
1476+ let row = self
1477+ . conn ( )
1478+ . query_row (
1479+ "SELECT status, deleted_sessions, deleted_summaries, message, started_at, finished_at \
1480+ FROM lifecycle_cleanup_jobs WHERE id = 1 LIMIT 1",
1481+ [ ] ,
1482+ |row| {
1483+ Ok ( LifecycleCleanupJobRow {
1484+ status : row. get ( 0 ) ?,
1485+ deleted_sessions : row. get :: < _ , i64 > ( 1 ) ?. max ( 0 ) as u32 ,
1486+ deleted_summaries : row. get :: < _ , i64 > ( 2 ) ?. max ( 0 ) as u32 ,
1487+ message : row. get ( 3 ) ?,
1488+ started_at : row. get ( 4 ) ?,
1489+ finished_at : row. get ( 5 ) ?,
1490+ } )
1491+ } ,
1492+ )
1493+ . optional ( ) ?;
1494+ Ok ( row)
1495+ }
1496+
14391497 // ── Sync cursor ────────────────────────────────────────────────────
14401498
14411499 pub fn get_sync_cursor ( & self , team_id : & str ) -> Result < Option < String > > {
@@ -2538,10 +2596,14 @@ mod tests {
25382596 migration_names. contains( & "local_0004_summary_batch_status" ) ,
25392597 "expected local_0004_summary_batch_status migration from opensession-api"
25402598 ) ;
2599+ assert ! (
2600+ migration_names. contains( & "local_0005_lifecycle_cleanup_status" ) ,
2601+ "expected local_0005_lifecycle_cleanup_status migration from opensession-api"
2602+ ) ;
25412603 assert_eq ! (
25422604 migration_names. len( ) ,
2543- 4 ,
2544- "local schema should include baseline + summary cache + vector index + summary batch status steps"
2605+ 5 ,
2606+ "local schema should include baseline + summary cache + vector index + summary batch status + lifecycle cleanup status steps"
25452607 ) ;
25462608
25472609 let manifest_dir = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) ;
@@ -3427,6 +3489,30 @@ mod tests {
34273489 assert_eq ! ( loaded. message. as_deref( ) , Some ( "processing summaries" ) ) ;
34283490 }
34293491
3492+ #[ test]
3493+ fn test_lifecycle_cleanup_job_round_trip ( ) {
3494+ let db = test_db ( ) ;
3495+ let payload = LifecycleCleanupJobRow {
3496+ status : "complete" . to_string ( ) ,
3497+ deleted_sessions : 3 ,
3498+ deleted_summaries : 7 ,
3499+ message : Some ( "cleanup complete" . to_string ( ) ) ,
3500+ started_at : Some ( "2026-03-06T01:00:00Z" . to_string ( ) ) ,
3501+ finished_at : Some ( "2026-03-06T01:00:04Z" . to_string ( ) ) ,
3502+ } ;
3503+ db. set_lifecycle_cleanup_job ( & payload)
3504+ . expect ( "set lifecycle cleanup job snapshot" ) ;
3505+
3506+ let loaded = db
3507+ . get_lifecycle_cleanup_job ( )
3508+ . expect ( "read lifecycle cleanup job snapshot" )
3509+ . expect ( "lifecycle cleanup row should exist" ) ;
3510+ assert_eq ! ( loaded. status, "complete" ) ;
3511+ assert_eq ! ( loaded. deleted_sessions, 3 ) ;
3512+ assert_eq ! ( loaded. deleted_summaries, 7 ) ;
3513+ assert_eq ! ( loaded. message. as_deref( ) , Some ( "cleanup complete" ) ) ;
3514+ }
3515+
34303516 #[ test]
34313517 fn test_session_count ( ) {
34323518 let db = test_db ( ) ;
0 commit comments