@@ -25,6 +25,7 @@ const JSON_WRITE_MAX_RETRIES: usize = 5;
2525const JSON_WRITE_RETRY_BASE_DELAY_MS : u64 = 30 ;
2626
2727static JSON_FILE_WRITE_LOCKS : OnceLock < Mutex < HashMap < PathBuf , Arc < Mutex < ( ) > > > > > = OnceLock :: new ( ) ;
28+ static SESSION_INDEX_LOCKS : OnceLock < Mutex < HashMap < PathBuf , Arc < Mutex < ( ) > > > > > = OnceLock :: new ( ) ;
2829
2930#[ derive( Debug , Clone , Serialize , Deserialize ) ]
3031struct StoredSessionMetadataFile {
@@ -285,6 +286,16 @@ impl PersistenceManager {
285286 . clone ( )
286287 }
287288
289+ async fn get_session_index_lock ( & self , workspace_path : & Path ) -> Arc < Mutex < ( ) > > {
290+ let index_path = self . index_path ( workspace_path) ;
291+ let registry = SESSION_INDEX_LOCKS . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
292+ let mut registry_guard = registry. lock ( ) . await ;
293+ registry_guard
294+ . entry ( index_path)
295+ . or_insert_with ( || Arc :: new ( Mutex :: new ( ( ) ) ) )
296+ . clone ( )
297+ }
298+
288299 fn build_temp_json_path ( path : & Path , attempt : usize ) -> BitFunResult < PathBuf > {
289300 let parent = path. parent ( ) . ok_or_else ( || {
290301 BitFunError :: io ( format ! (
@@ -468,7 +479,7 @@ impl PersistenceManager {
468479 }
469480 }
470481
471- async fn rebuild_index ( & self , workspace_path : & Path ) -> BitFunResult < Vec < SessionMetadata > > {
482+ async fn rebuild_index_locked ( & self , workspace_path : & Path ) -> BitFunResult < Vec < SessionMetadata > > {
472483 let sessions_root = self . ensure_project_sessions_dir ( workspace_path) . await ?;
473484 let mut metadata_list = Vec :: new ( ) ;
474485 let mut entries = fs:: read_dir ( & sessions_root)
@@ -515,7 +526,7 @@ impl PersistenceManager {
515526 Ok ( metadata_list)
516527 }
517528
518- async fn upsert_index_entry (
529+ async fn upsert_index_entry_locked (
519530 & self ,
520531 workspace_path : & Path ,
521532 metadata : & SessionMetadata ,
@@ -548,7 +559,7 @@ impl PersistenceManager {
548559 self . write_json_atomic ( & index_path, & index) . await
549560 }
550561
551- async fn remove_index_entry (
562+ async fn remove_index_entry_locked (
552563 & self ,
553564 workspace_path : & Path ,
554565 session_id : & str ,
@@ -568,6 +579,32 @@ impl PersistenceManager {
568579 self . write_json_atomic ( & index_path, & index) . await
569580 }
570581
582+ async fn rebuild_index ( & self , workspace_path : & Path ) -> BitFunResult < Vec < SessionMetadata > > {
583+ let lock = self . get_session_index_lock ( workspace_path) . await ;
584+ let _guard = lock. lock ( ) . await ;
585+ self . rebuild_index_locked ( workspace_path) . await
586+ }
587+
588+ async fn upsert_index_entry (
589+ & self ,
590+ workspace_path : & Path ,
591+ metadata : & SessionMetadata ,
592+ ) -> BitFunResult < ( ) > {
593+ let lock = self . get_session_index_lock ( workspace_path) . await ;
594+ let _guard = lock. lock ( ) . await ;
595+ self . upsert_index_entry_locked ( workspace_path, metadata) . await
596+ }
597+
598+ async fn remove_index_entry (
599+ & self ,
600+ workspace_path : & Path ,
601+ session_id : & str ,
602+ ) -> BitFunResult < ( ) > {
603+ let lock = self . get_session_index_lock ( workspace_path) . await ;
604+ let _guard = lock. lock ( ) . await ;
605+ self . remove_index_entry_locked ( workspace_path, session_id) . await
606+ }
607+
571608 pub async fn list_session_metadata (
572609 & self ,
573610 workspace_path : & Path ,
@@ -576,15 +613,28 @@ impl PersistenceManager {
576613 return Ok ( Vec :: new ( ) ) ;
577614 }
578615
616+ let lock = self . get_session_index_lock ( workspace_path) . await ;
617+ let _guard = lock. lock ( ) . await ;
579618 let index_path = self . index_path ( workspace_path) ;
580619 if let Some ( index) = self
581620 . read_json_optional :: < StoredSessionIndex > ( & index_path)
582621 . await ?
583622 {
623+ let has_stale_entry = index
624+ . sessions
625+ . iter ( )
626+ . any ( |metadata| !self . metadata_path ( workspace_path, & metadata. session_id ) . exists ( ) ) ;
627+ if has_stale_entry {
628+ warn ! (
629+ "Session index contains stale entries, rebuilding: {}" ,
630+ index_path. display( )
631+ ) ;
632+ return self . rebuild_index_locked ( workspace_path) . await ;
633+ }
584634 return Ok ( index. sessions ) ;
585635 }
586636
587- self . rebuild_index ( workspace_path) . await
637+ self . rebuild_index_locked ( workspace_path) . await
588638 }
589639
590640 pub async fn save_session_metadata (
@@ -944,6 +994,16 @@ impl PersistenceManager {
944994 workspace_path : & Path ,
945995 turn : & DialogTurnData ,
946996 ) -> BitFunResult < ( ) > {
997+ let mut metadata = self
998+ . load_session_metadata ( workspace_path, & turn. session_id )
999+ . await ?
1000+ . ok_or_else ( || {
1001+ BitFunError :: NotFound ( format ! (
1002+ "Session metadata not found: {}" ,
1003+ turn. session_id
1004+ ) )
1005+ } ) ?;
1006+
9471007 self . ensure_turns_dir ( workspace_path, & turn. session_id )
9481008 . await ?;
9491009
@@ -957,18 +1017,6 @@ impl PersistenceManager {
9571017 )
9581018 . await ?;
9591019
960- let mut metadata = self
961- . load_session_metadata ( workspace_path, & turn. session_id )
962- . await ?
963- . unwrap_or_else ( || {
964- SessionMetadata :: new (
965- turn. session_id . clone ( ) ,
966- "New Session" . to_string ( ) ,
967- "agentic" . to_string ( ) ,
968- "default" . to_string ( ) ,
969- )
970- } ) ;
971-
9721020 let turns = self
9731021 . load_session_turns ( workspace_path, & turn. session_id )
9741022 . await ?;
0 commit comments