summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/mesh.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/mesh.rs')
-rw-r--r--makima/src/server/handlers/mesh.rs170
1 files changed, 169 insertions, 1 deletions
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index b5ade53..3cd38b5 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -9,9 +9,10 @@ use axum::{
use uuid::Uuid;
use crate::db::models::{
+ AdhocTaskRequest, AdhocTaskResponse, ContractSummary, CreateContractRequest,
CreateTaskRequest, DaemonDirectory, DaemonDirectoriesResponse, DaemonListResponse,
SendMessageRequest, Task, TaskEventListResponse, TaskListResponse, TaskOutputEntry,
- TaskOutputResponse, TaskWithSubtasks, UpdateTaskRequest,
+ TaskOutputResponse, TaskWithSubtasks, UpdateTaskRequest, CONTRACT_TYPE_TASK,
};
use crate::db::repository::{self, RepositoryError};
use crate::server::auth::Authenticated;
@@ -359,6 +360,40 @@ pub async fn update_task(
}
});
}
+
+ // Auto-archive task-type contracts when task reaches terminal status
+ let terminal_statuses = ["done", "failed"];
+ if terminal_statuses.contains(&task.status.as_str()) {
+ let pool = pool.clone();
+ let owner_id = auth.owner_id;
+ let task_id = task.id;
+ tokio::spawn(async move {
+ if let Ok(Some(contract)) = repository::get_contract_for_owner(&pool, contract_id, owner_id).await {
+ if contract.contract_type == CONTRACT_TYPE_TASK {
+ // Archive the contract
+ let update_req = crate::db::models::UpdateContractRequest {
+ status: Some("archived".to_string()),
+ version: Some(contract.version),
+ ..Default::default()
+ };
+ if let Err(e) = repository::update_contract_for_owner(&pool, contract_id, owner_id, update_req).await {
+ tracing::warn!(
+ contract_id = %contract_id,
+ task_id = %task_id,
+ error = %e,
+ "Failed to auto-archive task-type contract"
+ );
+ } else {
+ tracing::info!(
+ contract_id = %contract_id,
+ task_id = %task_id,
+ "Auto-archived task-type contract on task completion"
+ );
+ }
+ }
+ }
+ });
+ }
}
Json(task).into_response()
@@ -3123,3 +3158,136 @@ pub async fn branch_from_checkpoint(
)
.into_response()
}
+
+// =============================================================================
+// Adhoc Task Endpoint
+// =============================================================================
+
+/// Create an adhoc (one-off) task without supervisor overhead.
+///
+/// This creates a minimal "task" type contract with a single task.
+/// The contract auto-archives when the task completes.
+#[utoipa::path(
+ post,
+ path = "/api/v1/tasks/adhoc",
+ request_body = AdhocTaskRequest,
+ responses(
+ (status = 201, description = "Adhoc task created", body = AdhocTaskResponse),
+ (status = 400, description = "Invalid request", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn create_adhoc_task(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Json(req): Json<AdhocTaskRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Generate a short unique name for the contract
+ let contract_name = format!("task-{}", &Uuid::new_v4().to_string()[..8]);
+
+ // 1. Create a minimal "task" type contract
+ let contract_req = CreateContractRequest {
+ name: contract_name,
+ description: Some(req.name.clone()),
+ contract_type: Some(CONTRACT_TYPE_TASK.to_string()),
+ initial_phase: Some("execute".to_string()), // Skip planning
+ autonomous_loop: Some(false),
+ };
+
+ let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await {
+ Ok(c) => c,
+ Err(e) => {
+ tracing::error!("Failed to create adhoc task contract: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ tracing::info!(
+ contract_id = %contract.id,
+ contract_type = %contract.contract_type,
+ "Created task-type contract for adhoc task"
+ );
+
+ // 2. Create the actual task (no supervisor)
+ let task_req = CreateTaskRequest {
+ contract_id: contract.id,
+ name: req.name,
+ description: None,
+ plan: req.plan,
+ is_supervisor: false,
+ parent_task_id: None,
+ priority: 0,
+ repository_url: req.repository_url,
+ base_branch: req.base_branch,
+ target_branch: None,
+ merge_mode: None,
+ target_repo_path: None,
+ completion_action: None,
+ continue_from_task_id: None,
+ copy_files: None,
+ checkpoint_sha: None,
+ };
+
+ let task = match repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
+ Ok(t) => t,
+ Err(e) => {
+ tracing::error!("Failed to create adhoc task: {}", e);
+ // Clean up the contract we just created
+ let _ = repository::delete_contract_for_owner(pool, contract.id, auth.owner_id).await;
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ tracing::info!(
+ task_id = %task.id,
+ contract_id = %contract.id,
+ "Created adhoc task"
+ );
+
+ // Build the contract summary for the response
+ let contract_summary = ContractSummary {
+ id: contract.id,
+ name: contract.name,
+ description: contract.description,
+ contract_type: contract.contract_type,
+ phase: contract.phase,
+ status: contract.status,
+ file_count: 0,
+ task_count: 1,
+ repository_count: 0,
+ version: contract.version,
+ created_at: contract.created_at,
+ };
+
+ (
+ StatusCode::CREATED,
+ Json(AdhocTaskResponse {
+ contract: contract_summary,
+ task,
+ }),
+ )
+ .into_response()
+}