summaryrefslogtreecommitdiff
path: root/makima/src/daemon/task
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/task')
-rw-r--r--makima/src/daemon/task/manager.rs108
1 files changed, 108 insertions, 0 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index fccebc5..c3ccfa4 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -980,6 +980,9 @@ pub struct TaskConfig {
pub bubblewrap: Option<crate::daemon::config::BubblewrapConfig>,
/// API URL for spawned tasks (HTTP endpoint for makima CLI).
pub api_url: String,
+ /// Interval in seconds between heartbeat commits (WIP checkpoints).
+ /// Set to 0 to disable. Default: 300 (5 minutes).
+ pub heartbeat_commit_interval_secs: u64,
}
impl Default for TaskConfig {
@@ -995,6 +998,7 @@ impl Default for TaskConfig {
disable_verbose: false,
bubblewrap: None,
api_url: "https://api.makima.jp".to_string(),
+ heartbeat_commit_interval_secs: 300, // 5 minutes
}
}
}
@@ -1587,6 +1591,7 @@ impl TaskManager {
git_user_email: self.git_user_email.clone(),
git_user_name: self.git_user_name.clone(),
api_url: self.config.api_url.clone(),
+ heartbeat_commit_interval_secs: self.config.heartbeat_commit_interval_secs,
}
}
@@ -2882,6 +2887,7 @@ struct TaskManagerInner {
git_user_email: Arc<RwLock<Option<String>>>,
git_user_name: Arc<RwLock<Option<String>>>,
api_url: String,
+ heartbeat_commit_interval_secs: u64,
}
impl TaskManagerInner {
@@ -3526,6 +3532,17 @@ impl TaskManagerInner {
startup_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let startup_deadline = tokio::time::Instant::now() + startup_timeout;
+ // Heartbeat commit interval (only active if configured and we have a git repo)
+ let heartbeat_enabled = self.heartbeat_commit_interval_secs > 0 && repo_source.is_some();
+ let mut heartbeat_interval = tokio::time::interval(
+ tokio::time::Duration::from_secs(
+ if heartbeat_enabled { self.heartbeat_commit_interval_secs } else { u64::MAX }
+ )
+ );
+ heartbeat_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
+ // Skip the first immediate tick
+ heartbeat_interval.tick().await;
+
loop {
tokio::select! {
maybe_line = process.next_output() => {
@@ -3645,6 +3662,23 @@ impl TaskManagerInner {
}
}
}
+ _ = heartbeat_interval.tick(), if heartbeat_enabled => {
+ // Create periodic heartbeat commit to preserve work-in-progress
+ match self.create_heartbeat_commit(task_id, &working_dir).await {
+ Ok(sha) => {
+ let msg = DaemonMessage::task_output(
+ task_id,
+ format!("[Heartbeat] Created WIP checkpoint: {}\n", &sha[..8]),
+ false,
+ );
+ let _ = ws_tx.send(msg).await;
+ }
+ Err(e) => {
+ // No changes to commit or git error - this is fine, just log at debug level
+ tracing::debug!(task_id = %task_id, error = %e, "Heartbeat commit skipped");
+ }
+ }
+ }
}
}
@@ -4115,6 +4149,79 @@ impl TaskManagerInner {
}
}
}
+
+ /// Create a heartbeat commit with all uncommitted changes (WIP checkpoint).
+ /// Returns the commit SHA on success, or an error message if nothing to commit.
+ async fn create_heartbeat_commit(
+ &self,
+ task_id: Uuid,
+ worktree_path: &std::path::Path,
+ ) -> Result<String, String> {
+ // 1. Check for uncommitted changes using git status --porcelain
+ let status_output = tokio::process::Command::new("git")
+ .current_dir(worktree_path)
+ .args(["status", "--porcelain"])
+ .output()
+ .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 commit".into());
+ }
+
+ // 2. Stage all changes
+ let add_output = 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));
+ }
+
+ // 3. Create WIP commit with timestamp
+ let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
+ let commit_msg = format!("[WIP] Heartbeat checkpoint - {}", timestamp);
+
+ let commit_output = tokio::process::Command::new("git")
+ .current_dir(worktree_path)
+ .args(["commit", "-m", &commit_msg])
+ .output()
+ .await
+ .map_err(|e| format!("Failed to run git commit: {}", e))?;
+
+ if !commit_output.status.success() {
+ let stderr = String::from_utf8_lossy(&commit_output.stderr);
+ return Err(format!("git commit failed: {}", stderr));
+ }
+
+ // 4. Get the commit SHA
+ let 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 !sha_output.status.success() {
+ let stderr = String::from_utf8_lossy(&sha_output.stderr);
+ return Err(format!("git rev-parse failed: {}", stderr));
+ }
+
+ let sha = String::from_utf8_lossy(&sha_output.stdout).trim().to_string();
+ tracing::info!(task_id = %task_id, sha = %sha, "Created heartbeat commit");
+
+ Ok(sha)
+ }
}
impl Clone for TaskManagerInner {
@@ -4130,6 +4237,7 @@ impl Clone for TaskManagerInner {
git_user_email: self.git_user_email.clone(),
git_user_name: self.git_user_name.clone(),
api_url: self.api_url.clone(),
+ heartbeat_commit_interval_secs: self.heartbeat_commit_interval_secs,
}
}
}