@@ -45,13 +45,10 @@ impl AppContext {
4545 pub fn build_status_snapshot_for_session ( & self , session_id : & str ) -> StatusPayload {
4646 let config = self . config ( ) ;
4747
48- // Search index status
49- let search_index_info = {
50- let index = self
51- . search_index ( )
52- . read ( )
53- . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) ;
54- match index. as_ref ( ) {
48+ // Search index status. Status is a control-path snapshot, so lock
49+ // pressure is represented directly instead of delaying the caller.
50+ let search_index_info = match self . search_index ( ) . try_read ( ) {
51+ Ok ( index) => match index. as_ref ( ) {
5552 Some ( idx) if idx. ready => {
5653 let file_count = idx. file_count ( ) ;
5754 let trigram_count = idx. trigram_count ( ) ;
@@ -70,78 +67,80 @@ impl AppContext {
7067 } ;
7168 serde_json:: json!( { "status" : status } )
7269 }
73- }
70+ } ,
71+ Err ( _) => serde_json:: json!( { "status" : "busy" } ) ,
7472 } ;
7573
76- // Semantic index status
77- let semantic_index_info = {
78- let status = self
79- . semantic_index_status ( )
80- . read ( )
81- . unwrap_or_else ( std:: sync:: PoisonError :: into_inner)
82- . clone ( ) ;
83- let refreshing_count = status. refreshing_count ( ) ;
84- let index = self
85- . semantic_index ( )
86- . read ( )
87- . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) ;
88- match index. as_ref ( ) {
89- Some ( idx) => {
90- let status_label = match status {
91- SemanticIndexStatus :: Ready { .. } => "ready" ,
92- _ => idx. status_label ( ) ,
93- } ;
94- serde_json:: json!( {
95- "status" : status_label,
96- "state" : status_label,
97- "refreshing_count" : refreshing_count,
98- "entries" : idx. entry_count( ) ,
99- "dimension" : idx. dimension( ) ,
100- "backend" : idx. backend_label( ) . unwrap_or( config. semantic_backend_label( ) ) ,
101- "model" : idx. model_label( ) . unwrap_or( config. semantic. model. as_str( ) ) ,
102- } )
74+ let semantic_status = self
75+ . semantic_index_status ( )
76+ . try_read ( )
77+ . ok ( )
78+ . map ( |status| status. clone ( ) ) ;
79+ let semantic_index_info = match semantic_status {
80+ None => serde_json:: json!( { "status" : "busy" , "state" : "busy" } ) ,
81+ Some ( status) => match self . semantic_index ( ) . try_read ( ) {
82+ Err ( _) => serde_json:: json!( { "status" : "busy" , "state" : "busy" } ) ,
83+ Ok ( index) => {
84+ let refreshing_count = status. refreshing_count ( ) ;
85+ match index. as_ref ( ) {
86+ Some ( idx) => {
87+ let status_label = match status {
88+ SemanticIndexStatus :: Ready { .. } => "ready" ,
89+ _ => idx. status_label ( ) ,
90+ } ;
91+ serde_json:: json!( {
92+ "status" : status_label,
93+ "state" : status_label,
94+ "refreshing_count" : refreshing_count,
95+ "entries" : idx. entry_count( ) ,
96+ "dimension" : idx. dimension( ) ,
97+ "backend" : idx. backend_label( ) . unwrap_or( config. semantic_backend_label( ) ) ,
98+ "model" : idx. model_label( ) . unwrap_or( config. semantic. model. as_str( ) ) ,
99+ } )
100+ }
101+ None => match status {
102+ SemanticIndexStatus :: Disabled => serde_json:: json!( {
103+ "status" : "disabled" ,
104+ "state" : "disabled" ,
105+ "refreshing_count" : 0 ,
106+ "backend" : config. semantic_backend_label( ) ,
107+ "model" : config. semantic. model. as_str( ) ,
108+ } ) ,
109+ SemanticIndexStatus :: Building {
110+ stage,
111+ files,
112+ entries_done,
113+ entries_total,
114+ } => serde_json:: json!( {
115+ "status" : "loading" ,
116+ "state" : "loading" ,
117+ "refreshing_count" : 0 ,
118+ "stage" : stage,
119+ "files" : files,
120+ "entries_done" : entries_done,
121+ "entries_total" : entries_total,
122+ "backend" : config. semantic_backend_label( ) ,
123+ "model" : config. semantic. model. as_str( ) ,
124+ } ) ,
125+ SemanticIndexStatus :: Ready { refreshing, .. } => serde_json:: json!( {
126+ "status" : "ready" ,
127+ "state" : "ready" ,
128+ "refreshing_count" : refreshing. len( ) ,
129+ "backend" : config. semantic_backend_label( ) ,
130+ "model" : config. semantic. model. as_str( ) ,
131+ } ) ,
132+ SemanticIndexStatus :: Failed ( error) => serde_json:: json!( {
133+ "status" : "failed" ,
134+ "state" : "failed" ,
135+ "refreshing_count" : 0 ,
136+ "error" : error,
137+ "backend" : config. semantic_backend_label( ) ,
138+ "model" : config. semantic. model. as_str( ) ,
139+ } ) ,
140+ } ,
141+ }
103142 }
104- None => match status {
105- SemanticIndexStatus :: Disabled => serde_json:: json!( {
106- "status" : "disabled" ,
107- "state" : "disabled" ,
108- "refreshing_count" : 0 ,
109- "backend" : config. semantic_backend_label( ) ,
110- "model" : config. semantic. model. as_str( ) ,
111- } ) ,
112- SemanticIndexStatus :: Building {
113- stage,
114- files,
115- entries_done,
116- entries_total,
117- } => serde_json:: json!( {
118- "status" : "loading" ,
119- "state" : "loading" ,
120- "refreshing_count" : 0 ,
121- "stage" : stage,
122- "files" : files,
123- "entries_done" : entries_done,
124- "entries_total" : entries_total,
125- "backend" : config. semantic_backend_label( ) ,
126- "model" : config. semantic. model. as_str( ) ,
127- } ) ,
128- SemanticIndexStatus :: Ready { refreshing, .. } => serde_json:: json!( {
129- "status" : "ready" ,
130- "state" : "ready" ,
131- "refreshing_count" : refreshing. len( ) ,
132- "backend" : config. semantic_backend_label( ) ,
133- "model" : config. semantic. model. as_str( ) ,
134- } ) ,
135- SemanticIndexStatus :: Failed ( error) => serde_json:: json!( {
136- "status" : "failed" ,
137- "state" : "failed" ,
138- "refreshing_count" : 0 ,
139- "error" : error,
140- "backend" : config. semantic_backend_label( ) ,
141- "model" : config. semantic. model. as_str( ) ,
142- } ) ,
143- } ,
144- }
143+ } ,
145144 } ;
146145
147146 // Disk cache sizes — scoped to the **current project** only.
@@ -258,6 +257,11 @@ impl AppContext {
258257 } ) ,
259258 None => serde_json:: Value :: Null ,
260259 } ;
260+ let memory_root = self
261+ . canonical_cache_root_opt ( )
262+ . or_else ( || config. project_root . clone ( ) ) ;
263+ let memory = serde_json:: to_value ( self . memory_snapshot ( memory_root. as_deref ( ) ) )
264+ . unwrap_or ( serde_json:: Value :: Null ) ;
261265
262266 serde_json:: json!( {
263267 "version" : env!( "CARGO_PKG_VERSION" ) ,
@@ -282,6 +286,7 @@ impl AppContext {
282286 "disk" : disk_info,
283287 "lsp_servers" : lsp_count,
284288 "symbol_cache" : symbol_cache_stats,
289+ "memory" : memory,
285290 "compression" : compression,
286291 "storage_dir" : storage_dir,
287292 // Project-wide (all sessions): total in-memory checkpoint count.
@@ -396,6 +401,18 @@ mod tests {
396401 assert_eq ! ( response. data[ "cache_role" ] , "worktree" ) ;
397402 }
398403
404+ #[ test]
405+ fn memory_snapshot_reports_contended_subsystem_as_busy ( ) {
406+ let ctx = AppContext :: new ( Box :: new ( TreeSitterProvider :: new ( ) ) , Config :: default ( ) ) ;
407+ let _semantic_writer = ctx. semantic_index ( ) . write ( ) . unwrap ( ) ;
408+ let status = ctx. build_status_snapshot ( ) ;
409+ assert_eq ! ( status[ "semantic_index" ] [ "status" ] , "busy" ) ;
410+ assert_eq ! (
411+ status[ "memory" ] [ "roots" ] [ "<unconfigured>" ] [ "semantic" ] [ "status" ] ,
412+ "busy"
413+ ) ;
414+ }
415+
399416 #[ test]
400417 fn status_status_bar_is_null_until_tier2_populated ( ) {
401418 let ctx = AppContext :: new ( Box :: new ( TreeSitterProvider :: new ( ) ) , Config :: default ( ) ) ;
0 commit comments