blob: 874ae1f045d0b5c613f3a96cfe7ecc7d0a83394e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use maka_event_log::sessions::{SessionMutation, SessionRemovalResult, SessionRetirement};
impl Owner {
pub async fn change_session(
self: &Arc<Self>,
input: ChangeSession,
) -> Result<SessionChanged, String> {
let runs = self.runs.lock().await;
if runs.closing {
return Ok(SessionChanged::Rejected {
reason: Rejection::Closed,
});
}
if !self.owns(&input.session_id) {
return Ok(SessionChanged::Rejected {
reason: Rejection::SessionUnavailable,
});
}
// Retry observes the durable deletion fence even after catalog removal.
if matches!(input.action, SessionAction::Remove)
&& self
.store
.session_retirement(&input.session_id)
.await
.map_err(|_| "agent_session_unavailable")?
.is_some_and(SessionRetirement::removes)
{
self.finish_removed_session(input.session_id.clone());
return Ok(SessionChanged::Removed {
session_id: input.session_id,
});
}
let Some(session) = self
.store
.get_session::<SessionConfiguration>(&input.session_id)
.await
.map_err(|_| "agent_session_unavailable")?
else {
return Ok(SessionChanged::Rejected {
reason: Rejection::SessionUnavailable,
});
};
if session.revision != input.expected_revision {
return Ok(SessionChanged::RevisionConflict {
expected: input.expected_revision,
actual: session.revision,
});
}
// Managed tasks retain their owner's lifecycle authority.
if self
.store
.session_manager(&input.session_id)
.await
.map_err(|_| "agent_session_unavailable")?
.is_some()
{
return Ok(SessionChanged::Rejected {
reason: Rejection::SessionUnavailable,
});
}
if runs.controls.contains_key(&input.session_id)
|| !self
.store
.message_queue(&input.session_id)
.await
.map_err(|_| "agent_queue_unavailable")?
.entries
.is_empty()
|| self.graph_selected(&input.session_id).await?
|| !matches!(
crate::service::summary(self, session).activity,
SessionActivity::Idle
)
{
return Ok(SessionChanged::Rejected {
reason: Rejection::Busy,
});
}
match input.action {
SessionAction::Remove => {
match self
.store
.begin_session_removal(&input.session_id, input.expected_revision)
.await
.map_err(|_| "agent_session_change_unknown")?
{
SessionRemovalResult::RevisionConflict { expected, actual } => {
Ok(SessionChanged::RevisionConflict { expected, actual })
}
SessionRemovalResult::Accepted(_) => {
// Delete the conversation only. User workspace files and
// independent copies are intentionally retained.
self.finish_removed_session(input.session_id.clone());
Ok(SessionChanged::Removed {
session_id: input.session_id,
})
}
}
}
SessionAction::Archive | SessionAction::Restore => {
match self
.store
.change_session_archived::<SessionConfiguration>(
&input.session_id,
Some(input.expected_revision),
matches!(input.action, SessionAction::Archive),
now(),
)
.await
{
Ok(SessionMutation::Committed(session)) => Ok(SessionChanged::Changed {
session: Box::new(crate::service::summary(self, session)),
}),
Ok(SessionMutation::RevisionConflict { expected, actual }) => {
Ok(SessionChanged::RevisionConflict { expected, actual })
}
Err(maka_event_log::StoreError::SessionBusy) => Ok(SessionChanged::Rejected {
reason: Rejection::Busy,
}),
Err(_) => Err("agent_session_change_unknown".into()),
}
}
}
}
fn finish_removed_session(self: &Arc<Self>, session: String) {
let owner = self.clone();
self.workers.spawn(async move {
loop {
if owner.stop.is_cancelled() {
return;
}
match owner.store.session_retirement(&session).await {
Ok(Some(SessionRetirement::Removed)) | Ok(None) => return,
Ok(Some(SessionRetirement::Removing)) => {}
_ => return,
}
if !matches!(
owner.store.session_retirement_ready(&session).await,
Ok(true)
) {
tokio::select! {
_ = owner.stop.cancelled() => return,
_ = tokio::time::sleep(Duration::from_millis(200)) => continue,
}
}
owner.engine.release_session_state(&session);
// The original deletion fence is restart recovery. Never undo it
// or acknowledge cleanup if durable owners are still unsettled.
if matches!(
owner.store.finish_session_retirement(&session).await,
Ok(SessionRetirement::Removed)
) {
return;
}
tokio::select! {
_ = owner.stop.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(1)) => {},
}
}
});
}
pub(super) fn recover_removed_sessions(self: &Arc<Self>) {
let owner = self.clone();
self.workers.spawn(async move {
let mut after = None;
loop {
if owner.stop.is_cancelled() {
return;
}
let Ok(sessions) = owner
.store
.pending_session_retirements(after.as_deref())
.await
else {
return;
};
if sessions.is_empty() {
return;
}
for session in &sessions {
if owner.owns(session) {
owner.finish_removed_session(session.clone());
}
}
after = sessions.last().cloned();
}
});
}
}