@@ -37,7 +37,10 @@ impl PromptVersions {
3737/// The RPG MCP server state.
3838#[ derive( Clone ) ]
3939pub ( crate ) struct RpgServer {
40- pub ( crate ) project_root : PathBuf ,
40+ /// Active project root. Mutable at runtime via the `set_project_root` tool
41+ /// so a single long-lived session can switch between projects without
42+ /// restart. Tools acquire a snapshot via [`RpgServer::project_root`].
43+ pub ( crate ) project_root_cell : Arc < RwLock < PathBuf > > ,
4144 pub ( crate ) graph : Arc < RwLock < Option < RPGraph > > > ,
4245 pub ( crate ) config : Arc < RwLock < RpgConfig > > ,
4346 pub ( crate ) lifting_session : Arc < RwLock < Option < LiftingSession > > > ,
@@ -69,13 +72,18 @@ pub(crate) struct RpgServer {
6972impl std:: fmt:: Debug for RpgServer {
7073 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
7174 f. debug_struct ( "RpgServer" )
72- . field ( "project_root" , & self . project_root )
75+ . field ( "project_root" , & "<lock>" )
7376 . field ( "lifting_session" , & "..." )
7477 . finish ( )
7578 }
7679}
7780
7881impl RpgServer {
82+ /// Snapshot of the active project root. Cheap — locks for a single clone.
83+ pub ( crate ) async fn project_root ( & self ) -> PathBuf {
84+ self . project_root_cell . read ( ) . await . clone ( )
85+ }
86+
7987 /// Create a new server, loading graph and config from `project_root` if present.
8088 pub ( crate ) fn new ( project_root : PathBuf ) -> Self {
8189 let graph = storage:: load ( & project_root) . ok ( ) ;
@@ -86,7 +94,7 @@ impl RpgServer {
8694 . map ( |s| s. entries )
8795 . unwrap_or_default ( ) ;
8896 Self {
89- project_root,
97+ project_root_cell : Arc :: new ( RwLock :: new ( project_root) ) ,
9098 graph : Arc :: new ( RwLock :: new ( graph) ) ,
9199 config : Arc :: new ( RwLock :: new ( config) ) ,
92100 lifting_session : Arc :: new ( RwLock :: new ( None ) ) ,
@@ -107,16 +115,17 @@ impl RpgServer {
107115
108116 /// Check if the loaded graph is stale (behind git HEAD) and return a notice string.
109117 pub ( crate ) async fn staleness_notice ( & self ) -> String {
118+ let project_root = self . project_root ( ) . await ;
110119 let guard = self . graph . read ( ) . await ;
111120 let Some ( graph) = guard. as_ref ( ) else {
112121 return String :: new ( ) ;
113122 } ;
114123 // Detect workdir changes (committed + staged + unstaged)
115- let Ok ( changes) = rpg_encoder:: evolution:: detect_workdir_changes ( & self . project_root , graph)
124+ let Ok ( changes) = rpg_encoder:: evolution:: detect_workdir_changes ( & project_root, graph)
116125 else {
117126 return String :: new ( ) ;
118127 } ;
119- let changes = rpg_encoder:: evolution:: filter_rpgignore_changes ( & self . project_root , changes) ;
128+ let changes = rpg_encoder:: evolution:: filter_rpgignore_changes ( & project_root, changes) ;
120129 let languages = Self :: resolve_languages ( & graph. metadata ) ;
121130 let source_changes = if languages. is_empty ( ) {
122131 changes
@@ -150,8 +159,9 @@ impl RpgServer {
150159 /// markers — the next query will retry, so transient failures don't
151160 /// silently leave the server stale.
152161 pub ( crate ) async fn auto_sync_if_stale ( & self ) -> String {
162+ let project_root = self . project_root ( ) . await ;
153163 // Step 1: Get current HEAD (cheap, just opens .git/HEAD)
154- let Ok ( current_head) = rpg_encoder:: evolution:: get_head_sha ( & self . project_root ) else {
164+ let Ok ( current_head) = rpg_encoder:: evolution:: get_head_sha ( & project_root) else {
155165 return self . staleness_notice ( ) . await ;
156166 } ;
157167
@@ -161,21 +171,19 @@ impl RpgServer {
161171 let Some ( graph) = guard. as_ref ( ) else {
162172 return String :: new ( ) ;
163173 } ;
164- let Ok ( changes) =
165- rpg_encoder:: evolution:: detect_workdir_changes ( & self . project_root , graph)
174+ let Ok ( changes) = rpg_encoder:: evolution:: detect_workdir_changes ( & project_root, graph)
166175 else {
167176 return String :: new ( ) ;
168177 } ;
169- let changes =
170- rpg_encoder:: evolution:: filter_rpgignore_changes ( & self . project_root , changes) ;
178+ let changes = rpg_encoder:: evolution:: filter_rpgignore_changes ( & project_root, changes) ;
171179 let languages = Self :: resolve_languages ( & graph. metadata ) ;
172180 let source_changes = if languages. is_empty ( ) {
173181 changes
174182 } else {
175183 rpg_encoder:: evolution:: filter_source_changes ( changes, & languages)
176184 } ;
177185 let paths = Self :: change_paths ( & source_changes) ;
178- let hash = Self :: compute_changeset_hash ( & source_changes, & self . project_root ) ;
186+ let hash = Self :: compute_changeset_hash ( & source_changes, & project_root) ;
179187 ( source_changes, paths, hash)
180188 } ;
181189
@@ -187,7 +195,7 @@ impl RpgServer {
187195 let last_paths = self . last_auto_sync_workdir_paths . read ( ) . await ;
188196 let mut effective = source_changes. clone ( ) ;
189197 for path in last_paths. difference ( & current_paths) {
190- let abs = self . project_root . join ( path) ;
198+ let abs = project_root. join ( path) ;
191199 if abs. is_file ( ) {
192200 effective. push ( rpg_encoder:: evolution:: FileChange :: Modified ( path. clone ( ) ) ) ;
193201 } else {
@@ -228,7 +236,7 @@ impl RpgServer {
228236 let qcache_result =
229237 rpg_parser:: paradigms:: query_engine:: QueryCache :: compile_all ( & paradigm_defs) ;
230238 let active_defs = rpg_parser:: paradigms:: detect_paradigms_toml (
231- & self . project_root ,
239+ & project_root,
232240 & detected_langs,
233241 & paradigm_defs,
234242 ) ;
@@ -243,13 +251,13 @@ impl RpgServer {
243251
244252 match rpg_encoder:: evolution:: run_update_from_changes (
245253 graph,
246- & self . project_root ,
254+ & project_root,
247255 effective_changes,
248256 pipeline. as_ref ( ) ,
249257 ) {
250258 Ok ( summary) => {
251259 graph. metadata . paradigms = paradigm_names;
252- let _ = storage:: save ( & self . project_root , graph) ;
260+ let _ = storage:: save ( & project_root, graph) ;
253261 * self . last_auto_sync_head . write ( ) . await = Some ( current_head) ;
254262 * self . last_auto_sync_changeset . write ( ) . await = Some ( current_changeset) ;
255263 * self . last_auto_sync_workdir_paths . write ( ) . await = current_paths;
@@ -405,7 +413,8 @@ impl RpgServer {
405413 }
406414 drop ( read) ;
407415
408- match storage:: load ( & self . project_root ) {
416+ let project_root = self . project_root ( ) . await ;
417+ match storage:: load ( & project_root) {
409418 Ok ( g) => {
410419 * self . graph . write ( ) . await = Some ( g) ;
411420 Ok ( ( ) )
@@ -417,10 +426,10 @@ impl RpgServer {
417426 }
418427
419428 /// Detailed staleness info: which source files changed (committed + staged + unstaged).
420- pub ( crate ) fn staleness_detail ( & self , graph : & RPGraph ) -> Option < String > {
421- let changes =
422- rpg_encoder:: evolution:: detect_workdir_changes ( & self . project_root , graph) . ok ( ) ?;
423- let changes = rpg_encoder:: evolution:: filter_rpgignore_changes ( & self . project_root , changes) ;
429+ pub ( crate ) async fn staleness_detail ( & self , graph : & RPGraph ) -> Option < String > {
430+ let project_root = self . project_root ( ) . await ;
431+ let changes = rpg_encoder:: evolution:: detect_workdir_changes ( & project_root, graph) . ok ( ) ?;
432+ let changes = rpg_encoder:: evolution:: filter_rpgignore_changes ( & project_root, changes) ;
424433 let languages = Self :: resolve_languages ( & graph. metadata ) ;
425434 let changes = rpg_encoder:: evolution:: filter_source_changes ( changes, & languages) ;
426435
@@ -471,7 +480,7 @@ impl RpgServer {
471480 } ;
472481
473482 // Check staleness
474- let stale_detail = self . staleness_detail ( graph) ;
483+ let stale_detail = self . staleness_detail ( graph) . await ;
475484 let graph_line = match & stale_detail {
476485 Some ( detail) => format ! (
477486 "graph: {} ({} entities, {} files)" ,
@@ -592,9 +601,10 @@ impl RpgServer {
592601 Ok ( out)
593602 }
594603
595- /// Load config synchronously (for contexts that cannot await).
596- pub ( crate ) fn get_config_blocking ( & self ) -> RpgConfig {
597- RpgConfig :: load ( & self . project_root ) . unwrap_or_default ( )
604+ /// Load the RPG config for the active project.
605+ pub ( crate ) async fn load_config ( & self ) -> RpgConfig {
606+ let project_root = self . project_root ( ) . await ;
607+ RpgConfig :: load ( & project_root) . unwrap_or_default ( )
598608 }
599609
600610 /// Lazy-initialize the embedding index on first semantic search.
@@ -612,8 +622,9 @@ impl RpgServer {
612622 return ;
613623 }
614624
625+ let project_root = self . project_root ( ) . await ;
615626 let updated_at = graph. updated_at . to_rfc3339 ( ) ;
616- match rpg_nav:: embeddings:: EmbeddingIndex :: load_or_init ( & self . project_root , & updated_at) {
627+ match rpg_nav:: embeddings:: EmbeddingIndex :: load_or_init ( & project_root, & updated_at) {
617628 Ok ( mut idx) => {
618629 // Incremental sync: only re-embed entities whose features changed
619630 if let Err ( e) = idx. sync ( graph) {
0 commit comments