summaryrefslogtreecommitdiff
path: root/makima/src/daemon
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-23 20:00:34 +0000
committersoryu <soryu@soryu.co>2026-01-23 20:00:34 +0000
commit12cb721dbbe571bd3b2766546b2105ef034e6cf3 (patch)
tree07a12739c23f1389a66090b8fc84dfffb1d99f82 /makima/src/daemon
parent9e286c146e29e714b3b209b4d948d75cce179b05 (diff)
downloadsoryu-12cb721dbbe571bd3b2766546b2105ef034e6cf3.tar.gz
soryu-12cb721dbbe571bd3b2766546b2105ef034e6cf3.zip
[WIP] Heartbeat checkpoint - 2026-01-23 20:00:34 UTC
Diffstat (limited to 'makima/src/daemon')
-rw-r--r--makima/src/daemon/api/supervisor.rs4
-rw-r--r--makima/src/daemon/cli/daemon.rs5
-rw-r--r--makima/src/daemon/config.rs17
-rw-r--r--makima/src/daemon/task/manager.rs25
-rw-r--r--makima/src/daemon/ws/protocol.rs4
5 files changed, 51 insertions, 4 deletions
diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs
index 74c27e0..64af2e8 100644
--- a/makima/src/daemon/api/supervisor.rs
+++ b/makima/src/daemon/api/supervisor.rs
@@ -17,6 +17,10 @@ pub struct SpawnTaskRequest {
pub parent_task_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checkpoint_sha: Option<String>,
+ /// Override max_iterations for this task (optional).
+ /// If not set, uses the daemon's configured max_iterations.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub max_iterations: Option<u32>,
}
#[derive(Serialize)]
diff --git a/makima/src/daemon/cli/daemon.rs b/makima/src/daemon/cli/daemon.rs
index c779d64..0dd11dc 100644
--- a/makima/src/daemon/cli/daemon.rs
+++ b/makima/src/daemon/cli/daemon.rs
@@ -38,4 +38,9 @@ pub struct DaemonArgs {
/// Requires bwrap to be installed on the system.
#[arg(long, env = "MAKIMA_DAEMON_BUBBLEWRAP")]
pub bubblewrap: bool,
+
+ /// Maximum iterations for autonomous task loops.
+ /// Set to 0 for unlimited (not recommended). Default: 10.
+ #[arg(long, env = "MAKIMA_MAX_ITERATIONS")]
+ pub max_iterations: Option<u32>,
}
diff --git a/makima/src/daemon/config.rs b/makima/src/daemon/config.rs
index 476b57e..79c9341 100644
--- a/makima/src/daemon/config.rs
+++ b/makima/src/daemon/config.rs
@@ -218,6 +218,11 @@ pub struct ProcessConfig {
/// Set to 0 to disable. Default: 300 (5 minutes).
#[serde(default = "default_heartbeat_commit_interval", alias = "heartbeatcommitintervalsecs")]
pub heartbeat_commit_interval_secs: u64,
+
+ /// Maximum iterations for autonomous task loops.
+ /// Set to 0 for unlimited (not recommended). Default: 10.
+ #[serde(default = "default_max_iterations", alias = "maxiterations")]
+ pub max_iterations: u32,
}
fn default_claude_command() -> String {
@@ -228,6 +233,10 @@ fn default_heartbeat_commit_interval() -> u64 {
300 // 5 minutes
}
+fn default_max_iterations() -> u32 {
+ 10
+}
+
fn default_max_tasks() -> u32 {
4
}
@@ -245,6 +254,7 @@ impl Default for ProcessConfig {
env_vars: HashMap::new(),
bubblewrap: BubblewrapConfig::default(),
heartbeat_commit_interval_secs: default_heartbeat_commit_interval(),
+ max_iterations: default_max_iterations(),
}
}
}
@@ -530,6 +540,11 @@ impl DaemonConfig {
config.process.bubblewrap.enabled = true;
}
+ // Apply max_iterations if CLI flag is set
+ if let Some(max_iterations) = args.max_iterations {
+ config.process.max_iterations = max_iterations;
+ }
+
// Validate required fields after all sources are merged
config.validate()?;
@@ -564,6 +579,8 @@ impl DaemonConfig {
default_timeout_secs: 0,
env_vars: HashMap::new(),
bubblewrap: BubblewrapConfig::default(),
+ heartbeat_commit_interval_secs: 300,
+ max_iterations: 10,
},
local_db: LocalDbConfig {
path: PathBuf::from("/tmp/makima-daemon-test/state.db"),
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index 555cd2a..179c07f 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -947,6 +947,8 @@ pub struct ManagedTask {
pub contract_id: Option<Uuid>,
/// Whether to run in autonomous loop mode.
pub autonomous_loop: bool,
+ /// Override max_iterations for this task (None = use config default).
+ pub max_iterations: Option<u32>,
/// Time task was created.
pub created_at: Instant,
/// Time task started running.
@@ -985,6 +987,9 @@ pub struct TaskConfig {
/// Interval in seconds between heartbeat commits (WIP checkpoints).
/// Set to 0 to disable. Default: 300 (5 minutes).
pub heartbeat_commit_interval_secs: u64,
+ /// Maximum iterations for autonomous task loops.
+ /// Set to 0 for unlimited (not recommended). Default: 10.
+ pub max_iterations: u32,
}
impl Default for TaskConfig {
@@ -1002,6 +1007,7 @@ impl Default for TaskConfig {
api_url: "https://api.makima.jp".to_string(),
api_key: String::new(),
heartbeat_commit_interval_secs: 300, // 5 minutes
+ max_iterations: 10,
}
}
}
@@ -1176,6 +1182,7 @@ impl TaskManager {
autonomous_loop,
resume_session,
conversation_history,
+ max_iterations,
} => {
tracing::info!(
task_id = %task_id,
@@ -1194,6 +1201,7 @@ impl TaskManager {
continue_from_task_id = ?continue_from_task_id,
copy_files = ?copy_files,
contract_id = ?contract_id,
+ max_iterations = ?max_iterations,
plan_len = plan.len(),
"Spawning new task"
);
@@ -1202,7 +1210,7 @@ impl TaskManager {
parent_task_id, depth, is_orchestrator, is_supervisor,
target_repo_path, completion_action, continue_from_task_id,
copy_files, contract_id, autonomous_loop, resume_session,
- conversation_history,
+ conversation_history, max_iterations,
).await?;
}
DaemonCommand::PauseTask { task_id } => {
@@ -1501,6 +1509,7 @@ impl TaskManager {
autonomous_loop: bool,
resume_session: bool,
conversation_history: Option<serde_json::Value>,
+ max_iterations: Option<u32>,
) -> TaskResult<()> {
tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, depth = depth, "=== SPAWN_TASK START ===");
@@ -1554,6 +1563,7 @@ impl TaskManager {
copy_files: copy_files.clone(),
contract_id,
autonomous_loop,
+ max_iterations,
created_at: Instant::now(),
started_at: None,
completed_at: None,
@@ -1578,7 +1588,7 @@ impl TaskManager {
task_id, task_name, plan, repo_url, base_branch, target_branch,
is_orchestrator, is_supervisor, target_repo_path, completion_action,
continue_from_task_id, copy_files, contract_id, autonomous_loop, resume_session,
- conversation_history,
+ conversation_history, max_iterations,
).await {
tracing::error!(task_id = %task_id, error = %e, "Task execution failed");
inner.mark_failed(task_id, &e.to_string()).await;
@@ -1604,6 +1614,7 @@ impl TaskManager {
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,
+ max_iterations: self.config.max_iterations,
}
}
@@ -2900,6 +2911,8 @@ struct TaskManagerInner {
git_user_name: Arc<RwLock<Option<String>>>,
api_url: String,
heartbeat_commit_interval_secs: u64,
+ /// Maximum iterations for autonomous task loops (from config).
+ max_iterations: u32,
}
impl TaskManagerInner {
@@ -2923,8 +2936,9 @@ impl TaskManagerInner {
autonomous_loop: bool,
resume_session: bool,
conversation_history: Option<serde_json::Value>,
+ max_iterations: Option<u32>,
) -> Result<(), DaemonError> {
- tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, resume_session = resume_session, "=== RUN_TASK START ===");
+ tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, resume_session = resume_session, max_iterations = ?max_iterations, "=== RUN_TASK START ===");
// If resuming session, try to find existing worktree first
let existing_worktree = if resume_session {
@@ -3567,7 +3581,10 @@ impl TaskManagerInner {
// For autonomous loop mode: track accumulated output for COMPLETION_GATE detection
let mut accumulated_output = String::new();
- let mut circuit_breaker = CircuitBreaker::new();
+ // Use task-specific max_iterations if provided, otherwise use config default
+ let effective_max_iterations = max_iterations.unwrap_or(self.max_iterations);
+ let mut circuit_breaker = CircuitBreaker::with_thresholds(3, 5, effective_max_iterations);
+ tracing::info!(task_id = %task_id, effective_max_iterations = effective_max_iterations, "Circuit breaker initialized");
let mut iteration_count = 0u32;
let mut final_exit_code: i64 = -1; // Track the final exit code across iterations
diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs
index d0bcc19..d9a91e9 100644
--- a/makima/src/daemon/ws/protocol.rs
+++ b/makima/src/daemon/ws/protocol.rs
@@ -387,6 +387,10 @@ pub enum DaemonCommand {
/// Used to inject previous conversation context into the prompt.
#[serde(rename = "conversationHistory", default)]
conversation_history: Option<serde_json::Value>,
+ /// Override max_iterations for this task (optional).
+ /// If not set, uses the daemon's configured max_iterations.
+ #[serde(rename = "maxIterations", default)]
+ max_iterations: Option<u32>,
},
/// Pause a running task.