diff options
Diffstat (limited to 'makima/src/daemon/task')
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 176 |
1 files changed, 112 insertions, 64 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 68c5c42..a24f527 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -5548,6 +5548,11 @@ impl TaskManagerInner { ); let _ = self.ws_tx.send(msg).await; } else { + // Create completion patch before notifying server + if let Err(e) = self.create_completion_patch(task_id, &working_dir).await { + tracing::debug!(task_id = %task_id, error = %e, "No completion patch created"); + } + let error = if success { None } else { @@ -5825,83 +5830,52 @@ impl TaskManagerInner { } } - /// Create an ephemeral patch of uncommitted changes and send to the server. - /// This does NOT create git commits or push - patches are stored in PostgreSQL only. + /// Create an ephemeral patch of all changes (committed + uncommitted) since the + /// merge-base with main/master and send to the server. + /// Stages and commits any uncommitted changes, then diffs against the merge-base. /// Returns the number of files changed on success, or an error message if nothing to patch. async fn create_ephemeral_patch( &self, task_id: Uuid, worktree_path: &std::path::Path, ) -> Result<i32, String> { - // 1. Get current HEAD SHA (base for the patch) - let base_sha_output = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["rev-parse", "HEAD"]) - .output() - .await - .map_err(|e| format!("Failed to run git rev-parse: {}", e))?; - - if !base_sha_output.status.success() { - let stderr = String::from_utf8_lossy(&base_sha_output.stderr); - return Err(format!("git rev-parse failed: {}", stderr)); + if !self.checkpoint_patches.enabled { + return Err("Checkpoint patches disabled".into()); } - let base_sha = String::from_utf8_lossy(&base_sha_output.stdout).trim().to_string(); - - // 2. Check for uncommitted changes using git status --porcelain - let status_output = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["status", "--porcelain"]) - .output() + // 1. Find merge-base with main/master (the fork point) + let base_sha = storage::get_merge_base_sha(worktree_path) .await - .map_err(|e| format!("Failed to run git status: {}", e))?; - - if !status_output.status.success() { - let stderr = String::from_utf8_lossy(&status_output.stderr); - return Err(format!("git status failed: {}", stderr)); - } - - let status = String::from_utf8_lossy(&status_output.stdout); - if status.trim().is_empty() { - return Err("No changes to patch".into()); - } + .map_err(|e| format!("Failed to get merge-base: {}", e))?; - // Count files with changes - let files_count = status.lines().count() as i32; - - // 3. Stage all changes (required for diff to include untracked files) - let add_output = tokio::process::Command::new("git") + // 2. Stage and commit any uncommitted changes so they're included in the diff + let _ = tokio::process::Command::new("git") .current_dir(worktree_path) .args(["add", "-A"]) .output() - .await - .map_err(|e| format!("Failed to run git add: {}", e))?; - - if !add_output.status.success() { - let stderr = String::from_utf8_lossy(&add_output.stderr); - return Err(format!("git add failed: {}", stderr)); - } + .await; - // 4. Create patch (diff of staged changes against HEAD) - if !self.checkpoint_patches.enabled { - // Reset staged changes and return - let _ = tokio::process::Command::new("git") - .current_dir(worktree_path) - .args(["reset", "HEAD"]) - .output() - .await; - return Err("Checkpoint patches disabled".into()); - } + // Check if there are staged changes to commit + let staged_check = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["diff", "--cached", "--quiet"]) + .output() + .await; - match storage::create_patch(worktree_path, &base_sha).await { - Ok((compressed_patch, patch_files_count)) => { - // Reset staged changes (we don't want to commit) + if let Ok(output) = staged_check { + if !output.status.success() { + // There are staged changes - commit them let _ = tokio::process::Command::new("git") .current_dir(worktree_path) - .args(["reset", "HEAD"]) + .args(["commit", "-m", "WIP: heartbeat checkpoint"]) .output() .await; + } + } + // 3. Create patch (diff merge-base..HEAD captures all work) + match storage::create_patch(worktree_path, &base_sha).await { + Ok((compressed_patch, patch_files_count)) => { // Check size limit if compressed_patch.len() > self.checkpoint_patches.max_patch_size_bytes { tracing::warn!( @@ -5929,10 +5903,10 @@ impl TaskManagerInner { let msg = DaemonMessage::CheckpointCreated { task_id, success: true, - commit_sha: None, // No git commit + commit_sha: None, branch_name: None, - checkpoint_number: None, // Server will assign - files_changed: None, // Detailed file info not tracked for ephemeral patches + checkpoint_number: None, + files_changed: None, lines_added: None, lines_removed: None, error: None, @@ -5943,18 +5917,92 @@ impl TaskManagerInner { }; let _ = self.ws_tx.send(msg).await; - Ok(files_count) + Ok(patch_files_count as i32) } Err(e) => { - // Reset staged changes + Err(format!("Failed to create patch: {}", e)) + } + } + } + + /// Create a completion patch capturing all changes (committed + uncommitted) since + /// the merge-base with main/master. Sent before TaskComplete so the server always + /// has a recoverable patch. All errors are non-fatal (logged, not propagated). + async fn create_completion_patch( + &self, + task_id: Uuid, + worktree_path: &std::path::Path, + ) -> Result<(), String> { + // 1. Stage all changes + let _ = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["add", "-A"]) + .output() + .await; + + // 2. Commit any staged changes so HEAD contains everything + let staged_check = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["diff", "--cached", "--quiet"]) + .output() + .await; + + if let Ok(output) = staged_check { + if !output.status.success() { let _ = tokio::process::Command::new("git") .current_dir(worktree_path) - .args(["reset", "HEAD"]) + .args(["commit", "-m", "WIP: task completion checkpoint"]) .output() .await; - Err(format!("Failed to create patch: {}", e)) } } + + // 3. Find merge-base with main/master + let base_sha = storage::get_merge_base_sha(worktree_path) + .await + .map_err(|e| format!("Failed to get merge-base: {}", e))?; + + // 4. Create patch (diff merge-base..HEAD) + let (compressed_patch, patch_files_count) = storage::create_patch(worktree_path, &base_sha) + .await + .map_err(|e| format!("Failed to create patch: {}", e))?; + + // 5. Check size limit + if compressed_patch.len() > self.checkpoint_patches.max_patch_size_bytes { + return Err(format!( + "Patch too large: {} bytes (max: {})", + compressed_patch.len(), + self.checkpoint_patches.max_patch_size_bytes + )); + } + + // 6. Send to server + let patch_data = base64::engine::general_purpose::STANDARD.encode(&compressed_patch); + let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: true, + commit_sha: None, + branch_name: None, + checkpoint_number: None, + files_changed: None, + lines_added: None, + lines_removed: None, + error: None, + message: format!("Completion patch - {}", timestamp), + patch_data: Some(patch_data), + patch_base_sha: Some(base_sha), + patch_files_count: Some(patch_files_count as i32), + }; + let _ = self.ws_tx.send(msg).await; + + tracing::info!( + task_id = %task_id, + files_count = patch_files_count, + "Created completion patch" + ); + + Ok(()) } } |
