diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 22:55:04 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-16 01:12:03 +0000 |
| commit | b69dc962cd99aa8b478b7c5facbd56bfb63eda27 (patch) | |
| tree | 9922f60b0da646eaf00165d5348b25e822dfd7b0 /makima/src/server/handlers/mesh.rs | |
| parent | 6ee2e75834bff187b8c262e0798ef365bc21cd59 (diff) | |
| download | soryu-b69dc962cd99aa8b478b7c5facbd56bfb63eda27.tar.gz soryu-b69dc962cd99aa8b478b7c5facbd56bfb63eda27.zip | |
Add Task Contract Type for one-off adhoc tasks (#2)
Diffstat (limited to 'makima/src/server/handlers/mesh.rs')
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 170 |
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() +} |
