1use std::cell::RefCell;
36use std::collections::HashMap;
37use std::sync::atomic::{AtomicBool, Ordering};
38use std::sync::{Arc, Mutex};
39
40use tokio::sync::Notify;
41
42#[derive(Clone, Debug)]
44pub struct CancellationDetails {
45 pub reason: String,
46 pub inject_reminder: bool,
47}
48
49#[derive(Debug)]
50struct Inner {
51 cancelled: AtomicBool,
52 completed: AtomicBool,
53 details: Mutex<Option<CancellationDetails>>,
54 notify: Notify,
55 completion_notify: Notify,
56}
57
58#[derive(Clone, Debug)]
60pub struct Handle {
61 pub session_id: String,
62 pub call_id: String,
63 pub tool_name: String,
64 inner: Arc<Inner>,
65}
66
67impl Handle {
68 fn new(session_id: String, call_id: String, tool_name: String) -> Self {
69 Self {
70 session_id,
71 call_id,
72 tool_name,
73 inner: Arc::new(Inner {
74 cancelled: AtomicBool::new(false),
75 completed: AtomicBool::new(false),
76 details: Mutex::new(None),
77 notify: Notify::new(),
78 completion_notify: Notify::new(),
79 }),
80 }
81 }
82
83 pub fn is_cancelled(&self) -> bool {
84 self.inner.cancelled.load(Ordering::SeqCst)
85 }
86
87 pub fn is_completed(&self) -> bool {
88 self.inner.completed.load(Ordering::SeqCst)
89 }
90
91 fn mark_completed(&self) {
94 if !self.inner.completed.swap(true, Ordering::SeqCst) {
95 self.inner.completion_notify.notify_waiters();
96 }
97 }
98
99 pub fn cancel(&self, reason: impl Into<String>, inject_reminder: bool) -> bool {
102 if self.inner.cancelled.swap(true, Ordering::SeqCst) {
103 return false;
104 }
105 let reason = reason.into();
106 let mut details = self
107 .inner
108 .details
109 .lock()
110 .unwrap_or_else(|err| err.into_inner());
111 *details = Some(CancellationDetails {
112 reason,
113 inject_reminder,
114 });
115 drop(details);
116 self.inner.notify.notify_waiters();
117 true
118 }
119
120 pub fn details(&self) -> Option<CancellationDetails> {
121 self.inner
122 .details
123 .lock()
124 .unwrap_or_else(|err| err.into_inner())
125 .clone()
126 }
127
128 pub fn reason(&self) -> Option<String> {
129 self.details().map(|d| d.reason)
130 }
131
132 pub async fn cancelled(&self) {
135 if self.is_cancelled() {
136 return;
137 }
138 let notified = self.inner.notify.notified();
139 if self.is_cancelled() {
140 return;
141 }
142 notified.await;
143 }
144
145 pub async fn completed(&self) {
149 if self.is_completed() {
150 return;
151 }
152 let notified = self.inner.completion_notify.notified();
153 if self.is_completed() {
154 return;
155 }
156 notified.await;
157 }
158}
159
160thread_local! {
161 static REGISTRY: RefCell<HashMap<(String, String), Handle>> =
162 RefCell::new(HashMap::new());
163}
164
165pub struct Guard {
168 session_id: String,
169 call_id: String,
170 handle: Handle,
171}
172
173impl Drop for Guard {
174 fn drop(&mut self) {
175 if self.call_id.is_empty() {
176 return;
177 }
178 self.handle.mark_completed();
179 REGISTRY.with(|registry| {
180 registry
181 .borrow_mut()
182 .remove(&(self.session_id.clone(), self.call_id.clone()));
183 });
184 }
185}
186
187pub fn register(
194 session_id: impl Into<String>,
195 call_id: impl Into<String>,
196 tool_name: impl Into<String>,
197) -> Option<(Handle, Guard)> {
198 let session_id = session_id.into();
199 let call_id = call_id.into();
200 let tool_name = tool_name.into();
201 if call_id.is_empty() {
202 return None;
203 }
204 let handle = Handle::new(session_id.clone(), call_id.clone(), tool_name);
205 let guard = Guard {
206 session_id: session_id.clone(),
207 call_id: call_id.clone(),
208 handle: handle.clone(),
209 };
210 REGISTRY.with(|registry| {
211 registry
212 .borrow_mut()
213 .insert((session_id, call_id), handle.clone());
214 });
215 Some((handle, guard))
216}
217
218pub fn lookup(session_id: &str, call_id: &str) -> Option<Handle> {
219 REGISTRY.with(|registry| {
220 registry
221 .borrow()
222 .get(&(session_id.to_string(), call_id.to_string()))
223 .cloned()
224 })
225}
226
227pub fn list_for_session(session_id: &str) -> Vec<Handle> {
230 REGISTRY.with(|registry| {
231 registry
232 .borrow()
233 .values()
234 .filter(|handle| handle.session_id == session_id)
235 .cloned()
236 .collect()
237 })
238}
239
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub enum CancelStatus {
243 Cancelled,
246 AlreadyCancelled,
248 NotFound,
251}
252
253impl CancelStatus {
254 pub fn as_str(self) -> &'static str {
255 match self {
256 Self::Cancelled => "cancelled",
257 Self::AlreadyCancelled => "already_cancelled",
258 Self::NotFound => "not_found",
259 }
260 }
261}
262
263#[derive(Clone, Debug)]
267pub struct CancelOutcome {
268 pub status: CancelStatus,
269 pub tool_name: Option<String>,
270 pub handle: Option<Handle>,
271}
272
273pub fn cancel(
275 session_id: &str,
276 call_id: &str,
277 reason: impl Into<String>,
278 inject_reminder: bool,
279) -> CancelOutcome {
280 let Some(handle) = lookup(session_id, call_id) else {
281 return CancelOutcome {
282 status: CancelStatus::NotFound,
283 tool_name: None,
284 handle: None,
285 };
286 };
287 let tool_name = Some(handle.tool_name.clone());
288 let status = if handle.cancel(reason, inject_reminder) {
289 CancelStatus::Cancelled
290 } else {
291 CancelStatus::AlreadyCancelled
292 };
293 CancelOutcome {
294 status,
295 tool_name,
296 handle: Some(handle),
297 }
298}
299
300pub fn reset_registry() {
307 REGISTRY.with(|registry| registry.borrow_mut().clear());
308}
309
310#[cfg(test)]
312pub fn registry_len() -> usize {
313 REGISTRY.with(|registry| registry.borrow().len())
314}
315
316#[cfg(test)]
317pub fn clear_registry_for_test() {
318 reset_registry();
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[tokio::test]
326 async fn handle_resolves_cancelled_future() {
327 clear_registry_for_test();
328 let (handle, _guard) = register("sess_1", "call_1", "bash").expect("registered");
329 assert!(!handle.is_cancelled());
330 let cancelled = handle.cancel("user requested stop", false);
331 assert!(cancelled);
332 handle.cancelled().await;
334 assert!(handle.is_cancelled());
335 assert_eq!(handle.reason().as_deref(), Some("user requested stop"));
336 }
337
338 #[tokio::test]
339 async fn cancel_returns_not_found_when_missing() {
340 clear_registry_for_test();
341 let outcome = cancel("sess_unknown", "call_unknown", "irrelevant", false);
342 assert_eq!(outcome.status, CancelStatus::NotFound);
343 assert_eq!(outcome.tool_name, None);
344 }
345
346 #[tokio::test]
347 async fn cancel_is_idempotent() {
348 clear_registry_for_test();
349 let (_handle, _guard) = register("sess", "call_2", "shell").expect("registered");
350 let first = cancel("sess", "call_2", "first", false);
351 let second = cancel("sess", "call_2", "second", false);
352 assert_eq!(first.status, CancelStatus::Cancelled);
353 assert_eq!(second.status, CancelStatus::AlreadyCancelled);
354 assert_eq!(first.tool_name.as_deref(), Some("shell"));
355 assert_eq!(second.tool_name.as_deref(), Some("shell"));
356 }
357
358 #[tokio::test]
359 async fn guard_unregisters_on_drop() {
360 clear_registry_for_test();
361 {
362 let _registration = register("sess", "call_g", "tool").expect("registered");
363 assert!(lookup("sess", "call_g").is_some());
364 }
365 assert!(lookup("sess", "call_g").is_none());
366 }
367
368 #[test]
369 fn cancelled_wakes_pending_waiter() {
370 let rt = tokio::runtime::Builder::new_current_thread()
371 .enable_all()
372 .build()
373 .expect("rt");
374 let local = tokio::task::LocalSet::new();
375 local.block_on(&rt, async {
376 clear_registry_for_test();
377 let (handle, _guard) = register("sess", "call_w", "tool").expect("registered");
378 let waiter_handle = handle.clone();
379 let task = tokio::task::spawn_local(async move { waiter_handle.cancelled().await });
380 tokio::task::yield_now().await;
381 handle.cancel("stopping", false);
382 tokio::time::timeout(std::time::Duration::from_secs(1), task)
383 .await
384 .expect("task should resolve quickly")
385 .expect("task did not panic");
386 });
387 }
388
389 #[tokio::test]
390 async fn list_for_session_filters_by_session() {
391 clear_registry_for_test();
392 let _r1 = register("sess_a", "call_x", "tool").expect("registered");
393 let _r2 = register("sess_a", "call_y", "tool").expect("registered");
394 let _r3 = register("sess_b", "call_x", "tool").expect("registered");
395 let mut ids: Vec<String> = list_for_session("sess_a")
396 .into_iter()
397 .map(|h| h.call_id)
398 .collect();
399 ids.sort();
400 assert_eq!(ids, vec!["call_x".to_string(), "call_y".to_string()]);
401 }
402}