66use std:: collections:: HashMap ;
77use std:: sync:: Arc ;
88use std:: sync:: Mutex ;
9- use std:: time:: { SystemTime , UNIX_EPOCH } ;
9+ use std:: time:: { Instant , SystemTime , UNIX_EPOCH } ;
1010
1111use bsk_protocol:: system:: { BrowserStatusEntry , SessionStatusEntry } ;
1212use bsk_protocol:: tools:: {
@@ -66,6 +66,9 @@ impl Session {
6666#[ derive( Debug , Default ) ]
6767pub struct SessionRegistry {
6868 inner : Mutex < HashMap < SessionId , Session > > ,
69+ /// Operational metadata kept outside the public `Session` wire/domain
70+ /// shape so idle enforcement does not break external struct users.
71+ last_activity : Mutex < HashMap < SessionId , Instant > > ,
6972}
7073
7174impl SessionRegistry {
@@ -100,10 +103,13 @@ impl SessionRegistry {
100103 }
101104
102105 pub fn insert ( & self , session : Session ) {
103- self . inner
106+ let session_id = session. id . clone ( ) ;
107+ let mut sessions = self . inner . lock ( ) . expect ( "session registry poisoned" ) ;
108+ sessions. insert ( session_id. clone ( ) , session) ;
109+ self . last_activity
104110 . lock ( )
105- . expect ( "session registry poisoned" )
106- . insert ( session . id . clone ( ) , session ) ;
111+ . expect ( "session activity registry poisoned" )
112+ . insert ( session_id , Instant :: now ( ) ) ;
107113 }
108114
109115 /// Reserve a fresh, collision-free [`SessionId`] under the registry
@@ -141,6 +147,10 @@ impl SessionRegistry {
141147 created_at_ms : now_ms_fn ( ) ,
142148 } ,
143149 ) ;
150+ self . last_activity
151+ . lock ( )
152+ . expect ( "session activity registry poisoned" )
153+ . insert ( candidate. clone ( ) , Instant :: now ( ) ) ;
144154 return Some ( candidate) ;
145155 }
146156 None
@@ -168,13 +178,23 @@ impl SessionRegistry {
168178 . lock ( )
169179 . expect ( "session registry poisoned" )
170180 . remove ( session_id) ;
181+ self . last_activity
182+ . lock ( )
183+ . expect ( "session activity registry poisoned" )
184+ . remove ( session_id) ;
171185 }
172186
173187 pub fn remove ( & self , id : & SessionId ) -> Option < Session > {
174- self . inner
188+ let removed = self
189+ . inner
175190 . lock ( )
176191 . expect ( "session registry poisoned" )
177- . remove ( id)
192+ . remove ( id) ;
193+ self . last_activity
194+ . lock ( )
195+ . expect ( "session activity registry poisoned" )
196+ . remove ( id) ;
197+ removed
178198 }
179199
180200 pub fn get ( & self , id : & SessionId ) -> Option < Session > {
@@ -185,6 +205,42 @@ impl SessionRegistry {
185205 . cloned ( )
186206 }
187207
208+ /// Record accepted or completed tool activity for a live session.
209+ pub fn touch ( & self , id : & SessionId ) -> bool {
210+ if !self
211+ . inner
212+ . lock ( )
213+ . expect ( "session registry poisoned" )
214+ . contains_key ( id)
215+ {
216+ return false ;
217+ }
218+ self . last_activity
219+ . lock ( )
220+ . expect ( "session activity registry poisoned" )
221+ . insert ( id. clone ( ) , Instant :: now ( ) ) ;
222+ true
223+ }
224+
225+ /// Return sessions whose last tool activity is at least `idle_for`
226+ /// old. The caller supplies `now` to keep boundary tests deterministic.
227+ pub fn idle_ids_at ( & self , idle_for : Duration , now : Instant ) -> Vec < SessionId > {
228+ let sessions = self . inner . lock ( ) . expect ( "session registry poisoned" ) ;
229+ let activity = self
230+ . last_activity
231+ . lock ( )
232+ . expect ( "session activity registry poisoned" ) ;
233+ sessions
234+ . values ( )
235+ . filter ( |session| {
236+ activity
237+ . get ( & session. id )
238+ . is_some_and ( |last| now. saturating_duration_since ( * last) >= idle_for)
239+ } )
240+ . map ( |session| session. id . clone ( ) )
241+ . collect ( )
242+ }
243+
188244 /// Drop all sessions owned by `browser_id` (e.g. on disconnect).
189245 pub fn purge_browser ( & self , browser_id : & BrowserId ) -> Vec < Session > {
190246 let mut guard = self . inner . lock ( ) . expect ( "session registry poisoned" ) ;
@@ -196,6 +252,13 @@ impl SessionRegistry {
196252 for s in & drained {
197253 guard. remove ( & s. id ) ;
198254 }
255+ let mut activity = self
256+ . last_activity
257+ . lock ( )
258+ . expect ( "session activity registry poisoned" ) ;
259+ for session in & drained {
260+ activity. remove ( & session. id ) ;
261+ }
199262 drained
200263 }
201264
0 commit comments