@@ -236,12 +236,25 @@ async def on_app_launched(self, app_info: Any) -> None:
236236 ),
237237 "timestamp" : _now (),
238238 }
239- self ._running_apps [app_info .tool_name ] = payload
239+ # Key by resource_uri when available so different tools sharing
240+ # the same resource replace each other rather than accumulating.
241+ app_key = app_info .resource_uri or app_info .tool_name
242+ self ._running_apps [app_key ] = payload
240243 await self ._broadcast (_envelope ("APP_LAUNCHED" , payload ))
241244
242245 async def on_app_closed (self , tool_name : str ) -> None :
243246 """Notify dashboard that an MCP App closed."""
244- self ._running_apps .pop (tool_name , None )
247+ # Try removing by tool_name first, then scan by resource_uri
248+ if tool_name not in self ._running_apps :
249+ to_remove = [
250+ k
251+ for k , v in self ._running_apps .items ()
252+ if v .get ("tool_name" ) == tool_name
253+ ]
254+ for k in to_remove :
255+ self ._running_apps .pop (k , None )
256+ else :
257+ self ._running_apps .pop (tool_name , None )
245258 await self ._broadcast (
246259 _envelope (
247260 "APP_CLOSED" ,
@@ -280,7 +293,10 @@ async def _on_client_connected(self, ws: Any) -> None:
280293 if self ._view_registry :
281294 await ws .send (
282295 _json .dumps (
283- _envelope ("VIEW_REGISTRY" , {"views" : self ._view_registry })
296+ _envelope (
297+ "VIEW_REGISTRY" ,
298+ {"agent_id" : self .agent_id , "views" : self ._view_registry },
299+ )
284300 )
285301 )
286302 # CONFIG_STATE (model, provider, servers, system prompt preview)
@@ -614,8 +630,8 @@ async def _handle_switch_session(self, msg: dict[str, Any]) -> None:
614630 if ctx .conversation_history :
615631 try :
616632 ctx .save_session ()
617- except Exception :
618- pass
633+ except Exception as exc :
634+ logger . warning ( "Failed to save current session before switch: %s" , exc )
619635
620636 # Clear and load the target session
621637 try :
@@ -757,9 +773,19 @@ async def request_tool_approval(
757773 "timestamp" : _now (),
758774 }
759775 # Create a future that the tool processor can await
760- fut : asyncio .Future [bool ] = asyncio .get_running_loop ().create_future ()
776+ loop = asyncio .get_running_loop ()
777+ fut : asyncio .Future [bool ] = loop .create_future ()
761778 self ._pending_approvals [call_id ] = fut
762779 await self ._broadcast (_envelope ("TOOL_APPROVAL_REQUEST" , payload ))
780+
781+ # Auto-deny after 5 minutes if no response (prevents hanging forever)
782+ def _timeout () -> None :
783+ if not fut .done ():
784+ logger .warning ("Tool approval for %s timed out after 5m" , tool_name )
785+ fut .set_result (False )
786+ self ._pending_approvals .pop (call_id , None )
787+
788+ loop .call_later (300 , _timeout )
763789 return fut
764790
765791 async def _handle_tool_approval_response (self , msg : dict [str , Any ]) -> None :
@@ -1064,9 +1090,11 @@ def _build_activity_history(self) -> list[dict[str, Any]] | None:
10641090 "result" : result_content ,
10651091 "error" : None ,
10661092 "success" : True ,
1067- "arguments" : self ._serialise (arguments )
1068- if arguments
1069- else None ,
1093+ "arguments" : (
1094+ self ._serialise (arguments )
1095+ if arguments
1096+ else None
1097+ ),
10701098 },
10711099 }
10721100 )
0 commit comments