blob: 7b36c95169d67f488405f24784f8d384fc492418 [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.
*/
//! History-seeded side conversations retain their original copy receipt.
use super::*;
use maka_agent_facts::session::{CopyPurpose, CopyRequest, CopyState};
use maka_capabilities::call::Scope;
use maka_event_log::{StoreError, sessions::SessionCopyResult};
use maka_plugin_core::operation::RecoveryRef;
impl Owner {
pub async fn fork(
self: &Arc<Self>,
input: Fork,
operation: &RecoveryRef,
scope: &Scope,
) -> Result<ForkResult, String> {
let operation = operation.clone();
self.resource_work(scope, move |owner, scope| {
Box::pin(async move { owner.fork_inner(input, &operation, &scope).await })
})
.await
}
async fn fork_inner(
&self,
input: Fork,
operation: &RecoveryRef,
scope: &Scope,
) -> Result<ForkResult, String> {
let request = CopyRequest {
source_session_id: input.session_id.clone(),
target_session_id: self.operation_id(operation),
expected_source_revision: input.expected_revision,
purpose: CopyPurpose::Branch {
turn_id: input.turn_id,
side_conversation: true,
},
};
// Original publication precedes source/configuration checks, including
// after source deletion. This never recreates a removed destination.
if let Some(receipt) = self
.store
.session_copy_receipt(&request.target_session_id)
.await
.map_err(fork_failure)?
{
if receipt.request != request || receipt.state != CopyState::Committed {
return Err("agent_fork_conflict".into());
}
let session = self.session(&request.target_session_id).await?;
return Ok(ForkResult::Forked {
session: Box::new(crate::service::summary(self, session)),
});
}
if let Some(reason) = metadata_rejection(&input.title, &input.labels) {
return Ok(ForkResult::Rejected { reason });
}
if self.runs.lock().await.closing {
return Ok(ForkResult::Rejected {
reason: Rejection::Closed,
});
}
if !self.owns(&input.session_id) {
return Ok(ForkResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
let Some(source) = self
.store
.get_session::<SessionConfiguration>(&input.session_id)
.await
.map_err(fork_failure)?
else {
return Ok(ForkResult::Rejected {
reason: Rejection::SessionUnavailable,
});
};
if source.revision != input.expected_revision {
return Ok(ForkResult::RevisionConflict {
expected: input.expected_revision,
actual: source.revision,
});
}
if !self
.project_accepts_session(&source.configuration.workspace)
.await?
{
return Ok(ForkResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
if workspace::validate(
&self.workspace_roots,
&source.configuration.workspace,
&self.managed_worktrees,
)
.await
.is_err()
{
return Ok(ForkResult::Rejected {
reason: Rejection::WorkspaceUnavailable,
});
}
if scope.cancellation.is_cancelled() || self.stop.is_cancelled() {
return Err("agent_fork_cancelled".into());
}
let parent = source.configuration;
let tools = parent.tools.map(|names| {
names
.into_iter()
.filter(|name| self.tools.contains(name))
.collect()
});
let access = match (parent.permissions.access, self.settings.permissions.access) {
(WorkspaceAccess::ReadOnly, _) | (_, WorkspaceAccess::ReadOnly) => {
WorkspaceAccess::ReadOnly
}
(WorkspaceAccess::WorkspaceWrite, _) | (_, WorkspaceAccess::WorkspaceWrite) => {
WorkspaceAccess::WorkspaceWrite
}
_ => WorkspaceAccess::DangerFullAccess,
};
let configuration = SessionConfiguration {
backend: parent.backend,
collaboration_mode: CollaborationMode::Agent,
tools,
title: input.title,
title_is_manual: true,
labels: input.labels,
workspace: parent.workspace,
permissions: WorkspacePermissions {
access,
request_permissions: parent.permissions.request_permissions
&& self.settings.permissions.request_permissions,
},
boundary_revision: 0,
};
let copied = match self
.store
.copy_session(request, &configuration, now())
.await
{
Ok(copied) => copied,
Err(
StoreError::HistoryTurnMissing
| StoreError::HistoryTurnSplit
| StoreError::HistoryUnsettled
| StoreError::HistoryProviderEffectUnresolved,
) => {
return Ok(ForkResult::Rejected {
reason: Rejection::HistoryUnavailable,
});
}
Err(StoreError::ManagedSession) => {
return Ok(ForkResult::Rejected {
reason: Rejection::ManagedSession,
});
}
Err(StoreError::SessionNotFound | StoreError::SessionRetired) => {
return Ok(ForkResult::Rejected {
reason: Rejection::SessionUnavailable,
});
}
Err(StoreError::SessionBusy) => {
return Ok(ForkResult::Rejected {
reason: Rejection::Busy,
});
}
Err(error) => return Err(fork_failure(error)),
};
match copied {
SessionCopyResult::Committed(session) => Ok(ForkResult::Forked {
session: Box::new(crate::service::summary(self, *session)),
}),
SessionCopyResult::SourceRevisionConflict { expected, actual } => {
Ok(ForkResult::RevisionConflict { expected, actual })
}
}
}
pub async fn fork_receipt(
self: &Arc<Self>,
input: ForkReceipt,
scope: &Scope,
) -> Result<ForkReceiptResult, String> {
input
.reference
.validate()
.map_err(|_| "agent_fork_reference_invalid")?;
let reference = &input.reference;
if reference.source.state_domain != self.data_source.state_domain
|| reference.source.deployment != self.data_source.deployment
|| reference.namespace != self.data_source.namespace
|| reference.export != maka_agent_api::EXPORT
|| reference.method != "fork"
{
return Err("agent_fork_reference_unavailable".into());
}
self.resource_work(scope, move |owner, _scope| {
Box::pin(async move {
let target = owner.operation_id(&input.reference);
let Some(receipt) = owner
.store
.session_copy_receipt(&target)
.await
.map_err(fork_failure)?
else {
return Ok(ForkReceiptResult { session: None });
};
if receipt.state != CopyState::Committed
|| receipt.request.target_session_id != target
|| !owner.owns(&receipt.request.source_session_id)
|| !matches!(
receipt.request.purpose,
CopyPurpose::Branch {
side_conversation: true,
..
}
)
{
return Err("agent_fork_reference_unavailable".into());
}
let session = owner
.store
.get_session::<SessionConfiguration>(&target)
.await
.map_err(fork_failure)?
.map(|session| Box::new(crate::service::summary(&owner, session)));
Ok(ForkReceiptResult { session })
})
})
.await
}
}
fn fork_failure(error: StoreError) -> String {
tracing::warn!(
stage = "session_fork",
cause = crate::diagnostics::store_cause(&error),
sql_code = crate::diagnostics::store_sql_code(&error),
"Session fork storage operation failed"
);
match error {
StoreError::CommitUnknown(_) | StoreError::OperationUnknown => "agent_fork_outcome_unknown",
_ => "agent_fork_failed",
}
.into()
}