diff options
| author | soryu <soryu@soryu.co> | 2026-01-17 05:37:47 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-17 05:38:07 +0000 |
| commit | 2f62df1cc89a23a5bd30e1a3f68a39bcfce9665c (patch) | |
| tree | c658378488cf6db293f7ca71d3ca957249a6309e /makima/src/daemon/task/manager.rs | |
| parent | 75d9644d44ba998a32ed14c072e883a75145ab72 (diff) | |
| download | soryu-2f62df1cc89a23a5bd30e1a3f68a39bcfce9665c.tar.gz soryu-2f62df1cc89a23a5bd30e1a3f68a39bcfce9665c.zip | |
Add heartbeat commits
Diffstat (limited to 'makima/src/daemon/task/manager.rs')
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 108 |
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, } } } |
