summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-21 17:31:46 +0000
committerGitHub <noreply@github.com>2026-01-21 17:31:46 +0000
commit94e5604e770d6589f786ea71e51738e21492f301 (patch)
tree6c9b0f32a8d77464bc1a5131ba0828d252851abc /makima/src/server
parentda246c4c4e23c9ad976705f9a3fa80e0d75b4425 (diff)
downloadsoryu-94e5604e770d6589f786ea71e51738e21492f301.tar.gz
soryu-94e5604e770d6589f786ea71e51738e21492f301.zip
Add task branching feature (#15)
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/contract_chat.rs16
-rw-r--r--makima/src/server/handlers/contracts.rs4
-rw-r--r--makima/src/server/handlers/mesh.rs269
-rw-r--r--makima/src/server/handlers/mesh_chat.rs4
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs4
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs8
-rw-r--r--makima/src/server/mod.rs61
-rw-r--r--makima/src/server/openapi.rs25
8 files changed, 362 insertions, 29 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 0f794c1..c94538d 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -1356,7 +1356,7 @@ async fn handle_contract_request(
};
let create_req = CreateTaskRequest {
- contract_id,
+ contract_id: Some(contract_id),
name: name.clone(),
description: None,
plan,
@@ -1372,6 +1372,8 @@ async fn handle_contract_request(
copy_files: None,
is_supervisor: false,
checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -1450,7 +1452,7 @@ async fn handle_contract_request(
);
let create_req = CreateTaskRequest {
- contract_id,
+ contract_id: Some(contract_id),
name: task_name.clone(),
description: Some(instruction.clone()),
plan,
@@ -1466,6 +1468,8 @@ async fn handle_contract_request(
copy_files: None,
is_supervisor: false,
checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2054,7 +2058,7 @@ async fn handle_contract_request(
for task_def in &tasks {
let create_req = CreateTaskRequest {
- contract_id,
+ contract_id: Some(contract_id),
name: task_def.name.clone(),
description: None,
plan: task_def.plan.clone(),
@@ -2070,6 +2074,8 @@ async fn handle_contract_request(
copy_files: None,
is_supervisor: false,
checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2564,7 +2570,7 @@ async fn handle_contract_request(
if include_action_items && !analysis.action_items.is_empty() {
for item in &analysis.action_items {
let task_req = CreateTaskRequest {
- contract_id: contract.id,
+ contract_id: Some(contract.id),
name: item.text.chars().take(100).collect(),
description: Some(format!("Action item from: {}", item.speaker)),
plan: item.text.clone(),
@@ -2584,6 +2590,8 @@ async fn handle_contract_request(
copy_files: None,
is_supervisor: false,
checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
if repository::create_task_for_owner(pool, owner_id, task_req).await.is_ok() {
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index 11337f2..462b385 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -287,7 +287,7 @@ pub async fn create_contract(
base_branch: None,
target_branch: None,
parent_task_id: None,
- contract_id: contract.id,
+ contract_id: Some(contract.id),
target_repo_path: None,
completion_action: None,
continue_from_task_id: None,
@@ -296,6 +296,8 @@ pub async fn create_contract(
checkpoint_sha: None,
priority: 0,
merge_mode: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await {
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index 275dc3c..99c3d9d 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::{
- CreateTaskRequest, DaemonDirectory, DaemonDirectoriesResponse, DaemonListResponse,
- SendMessageRequest, Task, TaskEventListResponse, TaskListResponse, TaskOutputEntry,
- TaskOutputResponse, TaskWithSubtasks, UpdateTaskRequest,
+ BranchTaskRequest, BranchTaskResponse, CreateTaskRequest, DaemonDirectory,
+ DaemonDirectoriesResponse, DaemonListResponse, SendMessageRequest, Task,
+ TaskEventListResponse, TaskListResponse, TaskOutputEntry, TaskOutputResponse,
+ TaskWithSubtasks, UpdateTaskRequest,
};
use crate::db::repository::{self, RepositoryError};
use crate::server::auth::Authenticated;
@@ -2196,7 +2197,7 @@ pub async fn reassign_task(
// Create a NEW task with the conversation context
let create_req = CreateTaskRequest {
- contract_id: task.contract_id.unwrap_or(Uuid::nil()),
+ contract_id: task.contract_id,
name: format!("{} (resumed)", task.name),
description: task.description.clone(),
plan: updated_plan.clone(),
@@ -2212,6 +2213,8 @@ pub async fn reassign_task(
continue_from_task_id: Some(id), // Continue from the old task's worktree if possible
copy_files: None,
checkpoint_sha: task.last_checkpoint_sha.clone(),
+ branched_from_task_id: None,
+ conversation_history: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -2913,7 +2916,7 @@ pub async fn fork_task(
// Create the new forked task
let create_req = CreateTaskRequest {
- contract_id: task.contract_id.unwrap_or(Uuid::nil()),
+ contract_id: task.contract_id,
name: req.new_task_name.clone(),
description: task.description.clone(),
plan: req.new_task_plan.clone(),
@@ -2929,6 +2932,8 @@ pub async fn fork_task(
continue_from_task_id: None,
copy_files: None,
checkpoint_sha: Some(checkpoint.commit_sha.clone()),
+ branched_from_task_id: None,
+ conversation_history: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3068,7 +3073,7 @@ pub async fn resume_from_checkpoint(
});
let create_req = CreateTaskRequest {
- contract_id: task.contract_id.unwrap_or(Uuid::nil()),
+ contract_id: task.contract_id,
name: task_name,
description: task.description.clone(),
plan: req.plan,
@@ -3084,6 +3089,8 @@ pub async fn resume_from_checkpoint(
continue_from_task_id: Some(task_id), // Copy worktree from original task
copy_files: None,
checkpoint_sha: Some(checkpoint.commit_sha.clone()),
+ branched_from_task_id: None,
+ conversation_history: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3304,3 +3311,253 @@ pub async fn branch_from_checkpoint(
)
.into_response()
}
+
+// =============================================================================
+// Task Branching
+// =============================================================================
+
+/// Branch a task, creating a new anonymous task from an existing task's conversation.
+///
+/// Creates a new task that:
+/// - Has no contract_id (anonymous task)
+/// - Has branched_from_task_id pointing to the source task
+/// - Optionally includes conversation history from the source task
+/// - Can be started on an available daemon
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/tasks/{id}/branch",
+ params(
+ ("id" = Uuid, Path, description = "Source task ID to branch from")
+ ),
+ request_body = BranchTaskRequest,
+ responses(
+ (status = 201, description = "Task branched successfully", body = BranchTaskResponse),
+ (status = 400, description = "Invalid request", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Source task not found", 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 branch_task(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(source_task_id): Path<Uuid>,
+ Json(req): Json<BranchTaskRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Get the source task (must belong to the same owner)
+ let source_task = match repository::get_task_for_owner(pool, source_task_id, auth.owner_id).await {
+ Ok(Some(task)) => task,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Source task not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get source task: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Build conversation history if requested
+ let (conversation_history, message_count) = if req.include_conversation {
+ match repository::get_task_output(pool, source_task_id, Some(500)).await {
+ Ok(events) => {
+ let entries: Vec<TaskOutputEntry> = events
+ .into_iter()
+ .filter_map(TaskOutputEntry::from_task_event)
+ .collect();
+ let count = entries.len();
+
+ // Convert entries to a JSON array for conversation_history
+ let history_json = serde_json::to_value(&entries).unwrap_or(serde_json::Value::Null);
+ (Some(history_json), count)
+ }
+ Err(e) => {
+ tracing::warn!("Failed to get task output for branching: {}", e);
+ (None, 0)
+ }
+ }
+ } else {
+ (None, 0)
+ };
+
+ // Generate task name if not provided
+ let task_name = req.name.unwrap_or_else(|| {
+ format!("{} (branch)", source_task.name)
+ });
+
+ // Create the branched task (anonymous - no contract_id)
+ let create_req = CreateTaskRequest {
+ contract_id: None, // Anonymous task
+ name: task_name,
+ description: Some(format!("Branched from task: {}", source_task.name)),
+ plan: req.message,
+ parent_task_id: None,
+ is_supervisor: false,
+ priority: source_task.priority,
+ repository_url: source_task.repository_url.clone(),
+ base_branch: source_task.base_branch.clone(),
+ target_branch: None, // Branched tasks don't auto-merge
+ merge_mode: None,
+ target_repo_path: source_task.target_repo_path.clone(),
+ completion_action: Some("none".to_string()), // Don't auto-complete
+ continue_from_task_id: Some(source_task_id), // Continue from source task's worktree
+ copy_files: None,
+ checkpoint_sha: None,
+ branched_from_task_id: Some(source_task_id),
+ conversation_history,
+ };
+
+ let task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
+ Ok(task) => task,
+ Err(e) => {
+ tracing::error!("Failed to create branched task: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Record history event for task branching
+ let _ = repository::record_history_event(
+ pool,
+ auth.owner_id,
+ None, // No contract for anonymous tasks
+ Some(task.id),
+ "task",
+ Some("branched"),
+ None,
+ serde_json::json!({
+ "name": &task.name,
+ "sourceTaskId": source_task_id,
+ "sourceTaskName": &source_task.name,
+ "messageCount": message_count,
+ }),
+ ).await;
+
+ // Try to find an available daemon to start the task
+ let daemon_id = state.daemon_connections
+ .iter()
+ .find(|d| d.value().owner_id == auth.owner_id)
+ .map(|d| d.value().id);
+
+ // If a daemon is available, start the task
+ if let Some(target_daemon_id) = daemon_id {
+ // Update task with daemon assignment
+ let update_req = UpdateTaskRequest {
+ status: Some("starting".to_string()),
+ daemon_id: Some(target_daemon_id),
+ ..Default::default()
+ };
+
+ if let Ok(Some(updated_task)) = repository::update_task_for_owner(pool, task.id, auth.owner_id, update_req).await {
+ // Send SpawnTask command to daemon
+ let command = DaemonCommand::SpawnTask {
+ task_id: task.id,
+ task_name: updated_task.name.clone(),
+ plan: updated_task.plan.clone(),
+ repo_url: updated_task.repository_url.clone(),
+ base_branch: updated_task.base_branch.clone(),
+ target_branch: updated_task.target_branch.clone(),
+ parent_task_id: None,
+ depth: 0,
+ is_orchestrator: false,
+ target_repo_path: updated_task.target_repo_path.clone(),
+ completion_action: updated_task.completion_action.clone(),
+ continue_from_task_id: updated_task.continue_from_task_id,
+ copy_files: None,
+ contract_id: None,
+ is_supervisor: false,
+ resume_session: message_count > 0, // Resume if we have conversation history
+ conversation_history: updated_task.conversation_state.clone(),
+ };
+
+ if let Err(e) = state.send_daemon_command(target_daemon_id, command).await {
+ tracing::warn!(
+ task_id = %task.id,
+ daemon_id = %target_daemon_id,
+ error = %e,
+ "Failed to send SpawnTask command for branched task, task created but not started"
+ );
+ // Task was created but not started - return without daemon_id
+ return (
+ StatusCode::CREATED,
+ Json(BranchTaskResponse {
+ task,
+ message_count,
+ daemon_id: None,
+ }),
+ )
+ .into_response();
+ }
+
+ tracing::info!(
+ task_id = %task.id,
+ source_task_id = %source_task_id,
+ daemon_id = %target_daemon_id,
+ message_count = message_count,
+ "Branched task created and started"
+ );
+
+ // Broadcast task update notification
+ state.broadcast_task_update(TaskUpdateNotification {
+ task_id: task.id,
+ owner_id: Some(auth.owner_id),
+ version: updated_task.version,
+ status: "starting".to_string(),
+ updated_fields: vec!["status".to_string(), "daemon_id".to_string()],
+ updated_by: "system".to_string(),
+ });
+
+ return (
+ StatusCode::CREATED,
+ Json(BranchTaskResponse {
+ task: updated_task,
+ message_count,
+ daemon_id: Some(target_daemon_id),
+ }),
+ )
+ .into_response();
+ }
+ }
+
+ // No daemon available or failed to start - return task without daemon_id
+ tracing::info!(
+ task_id = %task.id,
+ source_task_id = %source_task_id,
+ message_count = message_count,
+ "Branched task created (no daemon available to start)"
+ );
+
+ (
+ StatusCode::CREATED,
+ Json(BranchTaskResponse {
+ task,
+ message_count,
+ daemon_id: None,
+ }),
+ )
+ .into_response()
+}
diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs
index c468446..0fc5513 100644
--- a/makima/src/server/handlers/mesh_chat.rs
+++ b/makima/src/server/handlers/mesh_chat.rs
@@ -1002,7 +1002,7 @@ async fn handle_mesh_request(
};
let create_req = CreateTaskRequest {
- contract_id,
+ contract_id: Some(contract_id),
name: name.clone(),
description: None,
plan,
@@ -1018,6 +1018,8 @@ async fn handle_mesh_request(
copy_files: None,
is_supervisor: false,
checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs
index e5d33c7..6cdbba6 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -567,7 +567,7 @@ pub async fn spawn_task(
description: None,
plan: request.plan.clone(),
repository_url: repo_url.clone(),
- contract_id: request.contract_id,
+ contract_id: Some(request.contract_id),
parent_task_id: request.parent_task_id,
is_supervisor: false,
checkpoint_sha: request.checkpoint_sha.clone(),
@@ -579,6 +579,8 @@ pub async fn spawn_task(
completion_action: None,
continue_from_task_id: None,
copy_files: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
// Create task in DB
diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs
index 99f9ea7..3b71eca 100644
--- a/makima/src/server/handlers/transcript_analysis.rs
+++ b/makima/src/server/handlers/transcript_analysis.rs
@@ -344,7 +344,7 @@ pub async fn create_contract_from_analysis(
if request.include_action_items && !analysis.action_items.is_empty() {
for item in &analysis.action_items {
let task_req = models::CreateTaskRequest {
- contract_id: contract.id,
+ contract_id: Some(contract.id),
name: truncate_for_name(&item.text, 100),
description: Some(format!("Action item from transcript (Speaker: {})", item.speaker)),
plan: item.text.clone(),
@@ -364,6 +364,8 @@ pub async fn create_contract_from_analysis(
_ => 0,
},
merge_mode: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
@@ -515,7 +517,7 @@ pub async fn update_contract_from_analysis(
if request.create_tasks && !analysis.action_items.is_empty() {
for item in &analysis.action_items {
let task_req = models::CreateTaskRequest {
- contract_id: request.contract_id,
+ contract_id: Some(request.contract_id),
name: truncate_for_name(&item.text, 100),
description: Some(format!("Action item from {} (Speaker: {})", file.name, item.speaker)),
plan: item.text.clone(),
@@ -531,6 +533,8 @@ pub async fn update_contract_from_analysis(
checkpoint_sha: None,
priority: 0,
merge_mode: None,
+ branched_from_task_id: None,
+ conversation_history: None,
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index 7e31285..d575997 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -111,6 +111,8 @@ pub fn make_router(state: SharedState) -> Router {
.route("/mesh/tasks/{id}/fork", post(mesh::fork_task))
.route("/mesh/tasks/{id}/checkpoints/{cid}/resume", post(mesh::resume_from_checkpoint))
.route("/mesh/tasks/{id}/checkpoints/{cid}/branch", post(mesh::branch_from_checkpoint))
+ // Task branching endpoint
+ .route("/mesh/tasks/{id}/branch", post(mesh::branch_task))
// Supervisor endpoints (for supervisor.sh)
.route("/mesh/supervisor/contracts/{contract_id}/tasks", get(mesh_supervisor::list_contract_tasks))
.route("/mesh/supervisor/contracts/{contract_id}/tree", get(mesh_supervisor::get_contract_tree))
@@ -241,14 +243,23 @@ const DAEMON_CLEANUP_INTERVAL_SECS: u64 = 60;
/// Daemon heartbeat timeout in seconds (delete daemons older than this)
const DAEMON_HEARTBEAT_TIMEOUT_SECS: i64 = 120;
+/// Anonymous task cleanup interval in seconds (24 hours)
+const ANONYMOUS_TASK_CLEANUP_INTERVAL_SECS: u64 = 24 * 60 * 60;
+/// Maximum age in days for anonymous tasks before cleanup
+const ANONYMOUS_TASK_MAX_AGE_DAYS: i32 = 7;
+
/// Run the HTTP server with graceful shutdown support.
///
/// # Arguments
/// * `state` - Shared application state containing ML models
/// * `addr` - Address to bind to (e.g., "0.0.0.0:8080")
pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> {
- // Start background daemon cleanup task if database is available
+ // Start background cleanup tasks if database is available
if let Some(pool) = state.db_pool.clone() {
+ // Clone pool for each background task that needs it
+ let daemon_cleanup_pool = pool.clone();
+ let anonymous_task_cleanup_pool = pool.clone();
+
// Initial cleanup of any stale daemons from previous server run
match crate::db::repository::delete_stale_daemons(&pool, 0).await {
Ok(deleted) if deleted > 0 => {
@@ -263,7 +274,25 @@ pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> {
_ => {}
}
- // Spawn periodic cleanup task
+ // Initial cleanup of any stale anonymous tasks
+ match crate::db::repository::cleanup_stale_anonymous_tasks(
+ &pool,
+ ANONYMOUS_TASK_MAX_AGE_DAYS,
+ ).await {
+ Ok(deleted) if deleted > 0 => {
+ tracing::info!(
+ deleted = deleted,
+ max_age_days = ANONYMOUS_TASK_MAX_AGE_DAYS,
+ "Cleaned up stale anonymous tasks on startup"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(error = %e, "Failed to clean up stale anonymous tasks on startup");
+ }
+ _ => {}
+ }
+
+ // Spawn periodic daemon cleanup task
tokio::spawn(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(DAEMON_CLEANUP_INTERVAL_SECS)
@@ -271,7 +300,7 @@ pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> {
loop {
interval.tick().await;
match crate::db::repository::delete_stale_daemons(
- &pool,
+ &daemon_cleanup_pool,
DAEMON_HEARTBEAT_TIMEOUT_SECS,
).await {
Ok(deleted) if deleted > 0 => {
@@ -288,6 +317,32 @@ pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> {
}
}
});
+
+ // Spawn periodic anonymous task cleanup task (runs daily)
+ tokio::spawn(async move {
+ let mut interval = tokio::time::interval(
+ std::time::Duration::from_secs(ANONYMOUS_TASK_CLEANUP_INTERVAL_SECS)
+ );
+ loop {
+ interval.tick().await;
+ match crate::db::repository::cleanup_stale_anonymous_tasks(
+ &anonymous_task_cleanup_pool,
+ ANONYMOUS_TASK_MAX_AGE_DAYS,
+ ).await {
+ Ok(deleted) if deleted > 0 => {
+ tracing::info!(
+ deleted = deleted,
+ max_age_days = ANONYMOUS_TASK_MAX_AGE_DAYS,
+ "Cleaned up stale anonymous tasks"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(error = %e, "Failed to clean up stale anonymous tasks");
+ }
+ _ => {}
+ }
+ }
+ });
}
let app = make_router(state);
diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs
index 4daae3b..f8c5474 100644
--- a/makima/src/server/openapi.rs
+++ b/makima/src/server/openapi.rs
@@ -4,17 +4,17 @@ use utoipa::OpenApi;
use crate::db::models::{
AddLocalRepositoryRequest, AddRemoteRepositoryRequest, BranchInfo, BranchListResponse,
- ChangePhaseRequest, Contract, ContractChatHistoryResponse,
- ContractChatMessageRecord, ContractEvent, ContractListResponse, ContractRepository,
- ContractSummary, ContractWithRelations, CreateContractRequest, CreateFileRequest,
- CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse,
- DaemonDirectory, DaemonListResponse, File, FileListResponse, FileSummary, MergeCommitRequest,
- MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse, MergeSkipRequest,
- MergeStartRequest, MergeStatusResponse, MeshChatConversation, MeshChatHistoryResponse,
- MeshChatMessageRecord, RepositoryHistoryEntry, RepositoryHistoryListResponse,
- RepositorySuggestionsQuery, SendMessageRequest, Task, TaskEventListResponse, TaskListResponse,
- TaskSummary, TaskWithSubtasks, TranscriptEntry, UpdateContractRequest, UpdateFileRequest,
- UpdateTaskRequest,
+ BranchTaskRequest, BranchTaskResponse, ChangePhaseRequest, Contract,
+ ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, ContractListResponse,
+ ContractRepository, ContractSummary, ContractWithRelations, CreateContractRequest,
+ CreateFileRequest, CreateManagedRepositoryRequest, CreateTaskRequest, Daemon,
+ DaemonDirectoriesResponse, DaemonDirectory, DaemonListResponse, File, FileListResponse,
+ FileSummary, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest,
+ MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse,
+ MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, RepositoryHistoryEntry,
+ RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest, Task,
+ TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry,
+ UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
};
use crate::server::auth::{
ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse,
@@ -57,6 +57,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
mesh::get_daemon_directories,
mesh::clone_worktree,
mesh::check_target_exists,
+ mesh::branch_task,
mesh_chat::get_chat_history,
mesh_chat::clear_chat_history,
// Merge endpoints
@@ -123,6 +124,8 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
CreateTaskRequest,
UpdateTaskRequest,
SendMessageRequest,
+ BranchTaskRequest,
+ BranchTaskResponse,
TaskEventListResponse,
Daemon,
DaemonListResponse,