use actix_web::{web, HttpResponse, Result};
use crate::app_state::AppState;
pub async fn handler(state: web::Data<AppState>, path: web::Path<String>) -> Result<HttpResponse> {
let session_id = path.into_inner();
let ids_to_cancel: Vec<String> = match state.session_store.get_index_entry(&session_id).await {
Some(entry) if matches!(entry.kind, bamboo_agent_core::SessionKind::Root) => state
.session_store
.list_index_entries()
.await
.into_iter()
.filter(|e| e.root_session_id == session_id)
.map(|e| e.id)
.collect(),
Some(_) => vec![session_id.clone()],
None => vec![session_id.clone()],
};
for id in &ids_to_cancel {
if let Err(error) = state
.skill_manager
.release_activation_for_workspace(id, None)
.await
{
tracing::warn!(
"[{}] Failed to release deleted workflow activation snapshot: {}",
id,
error
);
}
}
let cancelled_runner = {
let mut runners = state.agent_runners.write().await;
let mut cancelled = false;
for id in ids_to_cancel.iter() {
if let Some(runner) = runners.remove(id) {
runner.cancel_token.cancel();
cancelled = true;
}
}
cancelled
};
let deleted_from_storage = match state.storage.delete_session(&session_id).await {
Ok(deleted) => deleted,
Err(error) => {
tracing::error!(
"[{}] Failed to delete session from storage: {}",
session_id,
error
);
return Ok(HttpResponse::InternalServerError().json(serde_json::json!({
"error": crate::error::error_value("Failed to delete session")
})));
}
};
let removed_from_memory = {
let mut removed = false;
for id in ids_to_cancel.iter() {
removed |= state.sessions.remove(id).is_some();
}
removed
};
{
let mut senders = state.session_event_senders.write().await;
for id in ids_to_cancel.iter() {
senders.remove(id);
}
}
let cancelled_in_flight = {
let mut tokens = state.cancel_tokens.write().await;
let mut cancelled = false;
for id in ids_to_cancel.iter() {
if let Some(token) = tokens.remove(id) {
token.cancel();
cancelled = true;
}
}
cancelled
};
if deleted_from_storage || removed_from_memory || cancelled_in_flight || cancelled_runner {
for id in ids_to_cancel.iter() {
state.account_sink.record(
Some(id),
&bamboo_agent_core::AgentEvent::SessionDeleted {
session_id: id.clone(),
},
);
}
tracing::info!(
"[{}] Session deleted successfully (storage: {}, memory: {}, cancelled: {}, runner_cancelled: {})",
session_id,
deleted_from_storage,
removed_from_memory,
cancelled_in_flight,
cancelled_runner
);
return Ok(HttpResponse::Ok().finish());
}
Ok(HttpResponse::NotFound().json(serde_json::json!({
"error": crate::error::error_value("Session not found")
})))
}
#[cfg(test)]
mod tests {
use super::*;
use bamboo_agent_core::storage::Storage;
use bamboo_agent_core::Session;
#[tokio::test]
async fn deleting_root_releases_root_and_child_workflow_activations() {
let directory = tempfile::tempdir().expect("tempdir");
let state = AppState::new(directory.path().to_path_buf())
.await
.expect("app state");
let root = Session::new("delete-root", "model");
let child = Session::new_child_of("delete-child", &root, "model", "child");
state
.session_store
.save_session(&root)
.await
.expect("save root index");
state
.session_store
.save_session(&child)
.await
.expect("save child index");
state.sessions.insert(
root.id.clone(),
std::sync::Arc::new(parking_lot::RwLock::new(root.clone())),
);
state.sessions.insert(
child.id.clone(),
std::sync::Arc::new(parking_lot::RwLock::new(child.clone())),
);
let ids = vec!["review".to_string()];
for session_id in [&root.id, &child.id] {
state
.skill_manager
.store()
.pin_current_activation(session_id, &ids, None)
.await
.expect("pin workflow activation");
}
let state = web::Data::new(state);
let response = handler(state.clone(), web::Path::from(root.id.clone()))
.await
.expect("delete response");
assert_eq!(response.status(), actix_web::http::StatusCode::OK);
for session_id in [&root.id, &child.id] {
assert!(state
.skill_manager
.store()
.activation_descriptor(session_id)
.await
.is_none());
}
}
}