summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-29 02:30:16 +0000
committersoryu <soryu@soryu.co>2026-01-29 02:30:16 +0000
commit7af8561677cfdcfd23d099a25783c7fef51d1ba6 (patch)
treea15cddfa7e5492c3d883419e60a1dad3c9d2d0f3
parentcfe3ea0aae878ae8f591acdc33a48332ac875b9e (diff)
downloadsoryu-7af8561677cfdcfd23d099a25783c7fef51d1ba6.tar.gz
soryu-7af8561677cfdcfd23d099a25783c7fef51d1ba6.zip
Fix worktree cleanup to not run for shared worktrees
-rw-r--r--makima/migrations/20260129000000_add_supervisor_worktree_task_id.sql7
-rw-r--r--makima/src/daemon/task/manager.rs23
-rw-r--r--makima/src/db/models.rs3
-rw-r--r--makima/src/db/repository.rs15
-rw-r--r--makima/src/server/handlers/contract_chat.rs4
-rw-r--r--makima/src/server/handlers/contracts.rs13
-rw-r--r--makima/src/server/handlers/mesh.rs4
-rw-r--r--makima/src/server/handlers/mesh_chat.rs1
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs5
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs2
10 files changed, 68 insertions, 9 deletions
diff --git a/makima/migrations/20260129000000_add_supervisor_worktree_task_id.sql b/makima/migrations/20260129000000_add_supervisor_worktree_task_id.sql
new file mode 100644
index 0000000..974644c
--- /dev/null
+++ b/makima/migrations/20260129000000_add_supervisor_worktree_task_id.sql
@@ -0,0 +1,7 @@
+-- Add supervisor_worktree_task_id column to tasks table.
+-- When set, indicates this task shares the worktree of the specified supervisor task
+-- and should NOT have its worktree deleted during cleanup (the supervisor owns the worktree).
+ALTER TABLE tasks ADD COLUMN supervisor_worktree_task_id UUID REFERENCES tasks(id) ON DELETE SET NULL;
+
+-- Create index for efficient lookups
+CREATE INDEX idx_tasks_supervisor_worktree_task_id ON tasks(supervisor_worktree_task_id) WHERE supervisor_worktree_task_id IS NOT NULL;
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index 8c5f8d7..1e05978 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -3305,12 +3305,27 @@ impl TaskManager {
task_id: Uuid,
) -> Result<(), DaemonError> {
// Get task's worktree path and branch
+ // If the task shares a supervisor's worktree, use the supervisor's worktree info
let task_info = {
let tasks = self.tasks.read().await;
- tasks.get(&task_id).map(|t| (
- t.worktree.as_ref().map(|w| w.path.clone()),
- t.worktree.as_ref().map(|w| w.branch.clone()),
- ))
+ if let Some(task) = tasks.get(&task_id) {
+ // Check if this task shares a supervisor's worktree
+ if let Some(supervisor_task_id) = task.supervisor_worktree_task_id {
+ // Use the supervisor's worktree
+ tasks.get(&supervisor_task_id).map(|supervisor| (
+ supervisor.worktree.as_ref().map(|w| w.path.clone()),
+ supervisor.worktree.as_ref().map(|w| w.branch.clone()),
+ ))
+ } else {
+ // Use the task's own worktree
+ Some((
+ task.worktree.as_ref().map(|w| w.path.clone()),
+ task.worktree.as_ref().map(|w| w.branch.clone()),
+ ))
+ }
+ } else {
+ None
+ }
};
let (worktree_path, branch) = match task_info {
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index d5f2814..9e624c9 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -665,6 +665,9 @@ pub struct CreateTaskRequest {
pub branched_from_task_id: Option<Uuid>,
/// Conversation history to initialize the task with (JSON array of messages)
pub conversation_history: Option<serde_json::Value>,
+ /// Task ID whose worktree this task shares. When set, this task reuses the supervisor's
+ /// worktree instead of creating its own, and should NOT have its worktree deleted during cleanup.
+ pub supervisor_worktree_task_id: Option<Uuid>,
}
/// Request payload for updating a task
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 7c9154f..b947cdd 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -691,9 +691,9 @@ pub async fn create_task(pool: &PgPool, req: CreateTaskRequest) -> Result<Task,
contract_id, parent_task_id, depth, name, description, plan, priority,
is_supervisor, is_red_team, repository_url, base_branch, target_branch, merge_mode,
target_repo_path, completion_action, continue_from_task_id, copy_files,
- branched_from_task_id, conversation_state
+ branched_from_task_id, conversation_state, supervisor_worktree_task_id
)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
RETURNING *
"#,
)
@@ -716,6 +716,7 @@ pub async fn create_task(pool: &PgPool, req: CreateTaskRequest) -> Result<Task,
.bind(&copy_files_json)
.bind(&req.branched_from_task_id)
.bind(&req.conversation_history)
+ .bind(&req.supervisor_worktree_task_id)
.fetch_one(pool)
.await
}
@@ -1105,9 +1106,9 @@ pub async fn create_task_for_owner(
owner_id, contract_id, parent_task_id, depth, name, description, plan, priority,
is_supervisor, is_red_team, repository_url, base_branch, target_branch, merge_mode,
target_repo_path, completion_action, continue_from_task_id, copy_files,
- branched_from_task_id, conversation_state
+ branched_from_task_id, conversation_state, supervisor_worktree_task_id
)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
RETURNING *
"#,
)
@@ -1131,6 +1132,7 @@ pub async fn create_task_for_owner(
.bind(&copy_files_json)
.bind(&req.branched_from_task_id)
.bind(&req.conversation_history)
+ .bind(&req.supervisor_worktree_task_id)
.fetch_one(pool)
.await
}
@@ -2745,6 +2747,9 @@ pub struct TaskWorktreeInfo {
pub id: Uuid,
pub daemon_id: Option<Uuid>,
pub overlay_path: Option<String>,
+ /// If set, this task shares the worktree of the specified supervisor task.
+ /// Should NOT have its worktree deleted during cleanup.
+ pub supervisor_worktree_task_id: Option<Uuid>,
}
/// List tasks in a contract with their daemon/worktree info.
@@ -2755,7 +2760,7 @@ pub async fn list_contract_tasks_with_worktree_info(
) -> Result<Vec<TaskWorktreeInfo>, sqlx::Error> {
sqlx::query_as::<_, TaskWorktreeInfo>(
r#"
- SELECT id, daemon_id, overlay_path
+ SELECT id, daemon_id, overlay_path, supervisor_worktree_task_id
FROM tasks
WHERE contract_id = $1 AND (daemon_id IS NOT NULL OR overlay_path IS NOT NULL)
"#,
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 98787be..c1ca3ed 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -1366,6 +1366,7 @@ async fn handle_contract_request(
checkpoint_sha: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -1463,6 +1464,7 @@ async fn handle_contract_request(
checkpoint_sha: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2197,6 +2199,7 @@ async fn handle_contract_request(
checkpoint_sha: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2717,6 +2720,7 @@ async fn handle_contract_request(
checkpoint_sha: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
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 3ad38da..7ed84e3 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -301,6 +301,7 @@ pub async fn create_contract(
merge_mode: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Supervisor uses its own worktree
};
match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await {
@@ -1673,7 +1674,19 @@ async fn cleanup_contract_worktrees(
);
// Send cleanup command to each task's daemon
+ // Skip tasks that share a supervisor's worktree (they don't own the worktree)
for task in tasks {
+ // Skip tasks that reuse the supervisor's worktree - the supervisor owns it
+ if task.supervisor_worktree_task_id.is_some() {
+ tracing::debug!(
+ task_id = %task.id,
+ supervisor_worktree_task_id = ?task.supervisor_worktree_task_id,
+ contract_id = %contract_id,
+ "Task shares supervisor worktree, skipping worktree cleanup"
+ );
+ continue;
+ }
+
if let Some(daemon_id) = task.daemon_id {
let cmd = crate::server::state::DaemonCommand::CleanupWorktree {
task_id: task.id,
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index 0dcab91..9ef6248 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -2623,6 +2623,7 @@ pub async fn reassign_task(
checkpoint_sha: task.last_checkpoint_sha.clone(),
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3397,6 +3398,7 @@ pub async fn fork_task(
checkpoint_sha: Some(checkpoint.commit_sha.clone()),
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3555,6 +3557,7 @@ pub async fn resume_from_checkpoint(
checkpoint_sha: Some(checkpoint.commit_sha.clone()),
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3891,6 +3894,7 @@ pub async fn branch_task(
checkpoint_sha: None,
branched_from_task_id: Some(source_task_id),
conversation_history,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
let task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs
index a74eb4f..623e66d 100644
--- a/makima/src/server/handlers/mesh_chat.rs
+++ b/makima/src/server/handlers/mesh_chat.rs
@@ -1021,6 +1021,7 @@ async fn handle_mesh_request(
checkpoint_sha: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
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 a0a3a96..6f17103 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -608,6 +608,9 @@ pub async fn spawn_task(
}
// Create task request
+ // Share supervisor's worktree by default; separate worktree only when explicitly requested
+ let supervisor_worktree_task_id = if request.use_own_worktree { None } else { Some(supervisor_id) };
+
let create_req = CreateTaskRequest {
name: request.name.clone(),
description: None,
@@ -628,6 +631,7 @@ pub async fn spawn_task(
copy_files: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id,
};
// Create task in DB
@@ -2650,6 +2654,7 @@ pub async fn spawn_red_team_task(
checkpoint_sha: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Red team uses its own working area
};
// Create task in DB
diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs
index 3c283da..0a6ac7f 100644
--- a/makima/src/server/handlers/transcript_analysis.rs
+++ b/makima/src/server/handlers/transcript_analysis.rs
@@ -370,6 +370,7 @@ pub async fn create_contract_from_analysis(
merge_mode: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
@@ -540,6 +541,7 @@ pub async fn update_contract_from_analysis(
merge_mode: None,
branched_from_task_id: None,
conversation_history: None,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {