diff options
Diffstat (limited to 'makima/src')
| -rw-r--r-- | makima/src/bin/makima.rs | 1 | ||||
| -rw-r--r-- | makima/src/daemon/config.rs | 56 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 111 | ||||
| -rw-r--r-- | makima/src/daemon/task/state.rs | 8 | ||||
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 4 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 19 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 5 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 10 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_chat.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 4 | ||||
| -rw-r--r-- | makima/src/server/handlers/transcript_analysis.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 3 |
13 files changed, 207 insertions, 19 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 96dc252..b82ad62 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -242,6 +242,7 @@ async fn run_daemon( api_key: config.server.api_key.clone(), heartbeat_commit_interval_secs: config.process.heartbeat_commit_interval_secs, checkpoint_patches: config.process.checkpoint_patches.clone(), + autonomous_loop_config: config.autonomous_loop.clone(), }; // Create task manager with local database for crash recovery diff --git a/makima/src/daemon/config.rs b/makima/src/daemon/config.rs index b7cb1e8..735a56a 100644 --- a/makima/src/daemon/config.rs +++ b/makima/src/daemon/config.rs @@ -37,6 +37,57 @@ fn default_true() -> bool { true } +/// Autonomous loop configuration for controlling iteration limits. +/// Inspired by Ralph's pattern for preventing runaway autonomous loops. +#[derive(Debug, Clone, Deserialize)] +#[serde(default)] +pub struct AutonomousLoopConfig { + /// Default maximum iterations for autonomous loop mode (default: 10). + /// Tasks will stop after this many iterations if no COMPLETION_GATE with ready: true is found. + #[serde(default = "default_max_iterations", alias = "defaultmaxiterations")] + pub default_max_iterations: u32, + + /// Hard limit on maximum iterations (default: 50). + /// This is an absolute maximum that cannot be exceeded, even if task specifies higher. + #[serde(default = "default_hard_limit", alias = "hardlimit")] + pub hard_limit: u32, + + /// Number of consecutive runs without file changes before stopping (default: 3). + #[serde(default = "default_no_change_threshold", alias = "nochangethreshold")] + pub no_change_threshold: u32, + + /// Number of consecutive runs with same error before stopping (default: 5). + #[serde(default = "default_same_error_threshold", alias = "sameerrorthreshold")] + pub same_error_threshold: u32, +} + +fn default_max_iterations() -> u32 { + 10 +} + +fn default_hard_limit() -> u32 { + 50 +} + +fn default_no_change_threshold() -> u32 { + 3 +} + +fn default_same_error_threshold() -> u32 { + 5 +} + +impl Default for AutonomousLoopConfig { + fn default() -> Self { + Self { + default_max_iterations: default_max_iterations(), + hard_limit: default_hard_limit(), + no_change_threshold: default_no_change_threshold(), + same_error_threshold: default_same_error_threshold(), + } + } +} + /// Root daemon configuration. #[derive(Debug, Clone, Deserialize)] pub struct DaemonConfig { @@ -63,6 +114,10 @@ pub struct DaemonConfig { /// Repositories to auto-clone on startup. #[serde(default)] pub repos: ReposConfig, + + /// Autonomous loop configuration for iteration limits. + #[serde(default)] + pub autonomous_loop: AutonomousLoopConfig, } /// Server connection configuration. @@ -626,6 +681,7 @@ impl DaemonConfig { }, logging: LoggingConfig::default(), repos: ReposConfig::default(), + autonomous_loop: AutonomousLoopConfig::default(), } } } diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 3fdde9b..9020f27 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -953,6 +953,8 @@ pub struct ManagedTask { pub concurrency_key: Uuid, /// Whether to run in autonomous loop mode. pub autonomous_loop: bool, + /// Maximum iterations for autonomous loop mode (None = use default from config). + pub max_iterations: Option<u32>, /// Time task was created. pub created_at: Instant, /// Time task started running. @@ -995,6 +997,8 @@ pub struct TaskConfig { pub heartbeat_commit_interval_secs: u64, /// Checkpoint patch storage configuration. pub checkpoint_patches: CheckpointPatchConfig, + /// Autonomous loop configuration for iteration limits. + pub autonomous_loop_config: crate::daemon::config::AutonomousLoopConfig, } impl Default for TaskConfig { @@ -1014,6 +1018,7 @@ impl Default for TaskConfig { api_key: String::new(), heartbeat_commit_interval_secs: 300, // 5 minutes checkpoint_patches: CheckpointPatchConfig::default(), + autonomous_loop_config: crate::daemon::config::AutonomousLoopConfig::default(), } } } @@ -1650,6 +1655,7 @@ impl TaskManager { conversation_history, patch_data, patch_base_sha, + max_iterations, } => { tracing::info!( task_id = %task_id, @@ -1662,6 +1668,7 @@ impl TaskManager { is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, autonomous_loop = autonomous_loop, + max_iterations = ?max_iterations, resume_session = resume_session, target_repo_path = ?target_repo_path, completion_action = ?completion_action, @@ -1676,7 +1683,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, patch_data, patch_base_sha, + conversation_history, patch_data, patch_base_sha, max_iterations, ).await?; } DaemonCommand::PauseTask { task_id } => { @@ -1776,6 +1783,7 @@ impl TaskManager { None, // conversation_history - not needed for fresh respawn None, // patch_data - not available for respawn None, // patch_base_sha - not available for respawn + None, // max_iterations - not used for supervisors ).await { tracing::error!( task_id = %task_id, @@ -2004,6 +2012,7 @@ impl TaskManager { conversation_history: Option<serde_json::Value>, patch_data: Option<String>, patch_base_sha: Option<String>, + max_iterations: Option<u32>, ) -> TaskResult<()> { tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, depth = depth, patch_available = patch_data.is_some(), "=== SPAWN_TASK START ==="); @@ -2054,6 +2063,7 @@ impl TaskManager { contract_id, concurrency_key, autonomous_loop, + max_iterations, created_at: Instant::now(), started_at: None, completed_at: None, @@ -2080,7 +2090,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, patch_data, patch_base_sha, + conversation_history, patch_data, patch_base_sha, max_iterations, ).await { tracing::error!(task_id = %task_id, error = %e, "Task execution failed"); inner.mark_failed(task_id, &e.to_string()).await; @@ -2112,6 +2122,7 @@ impl TaskManager { contract_task_counts: self.contract_task_counts.clone(), checkpoint_patches: self.config.checkpoint_patches.clone(), local_db: self.local_db.clone(), + autonomous_loop_config: self.config.autonomous_loop_config.clone(), } } @@ -3433,6 +3444,8 @@ struct TaskManagerInner { checkpoint_patches: CheckpointPatchConfig, /// Local SQLite database for crash recovery. local_db: Arc<std::sync::Mutex<LocalDb>>, + /// Autonomous loop configuration for iteration limits. + autonomous_loop_config: crate::daemon::config::AutonomousLoopConfig, } impl TaskManagerInner { @@ -3486,6 +3499,7 @@ impl TaskManagerInner { conversation_history: Option<serde_json::Value>, patch_data: Option<String>, patch_base_sha: Option<String>, + max_iterations: Option<u32>, ) -> Result<(), DaemonError> { tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, resume_session = resume_session, has_patch = patch_data.is_some(), "=== RUN_TASK START ==="); @@ -4208,9 +4222,31 @@ 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(); + + // Calculate effective max iterations: use task-specific value if provided, + // otherwise use daemon config default, but never exceed hard limit + let loop_config = &self.autonomous_loop_config; + let effective_max_iterations = max_iterations + .unwrap_or(loop_config.default_max_iterations) + .min(loop_config.hard_limit); + + tracing::info!( + task_id = %task_id, + task_max_iterations = ?max_iterations, + config_default = loop_config.default_max_iterations, + hard_limit = loop_config.hard_limit, + effective_max_iterations = effective_max_iterations, + "Autonomous loop configuration" + ); + + let mut circuit_breaker = CircuitBreaker::with_thresholds( + loop_config.no_change_threshold, + loop_config.same_error_threshold, + effective_max_iterations, + ); let mut iteration_count = 0u32; let mut final_exit_code: i64 = -1; // Track the final exit code across iterations + let mut iteration_limit_reached = false; // Track if we hit max iterations // Autonomous loop: we may run multiple iterations 'autonomous_loop: loop { @@ -4467,17 +4503,20 @@ impl TaskManagerInner { let error = gate.blockers.as_ref().and_then(|b| b.first()).map(|s| s.as_str()); if !circuit_breaker.record_iteration(had_progress, error) { - // Circuit breaker tripped + // Circuit breaker tripped - check if it was due to max iterations + let reason = circuit_breaker.open_reason.as_deref().unwrap_or("Unknown reason"); + if reason.contains("Maximum iterations") { + iteration_limit_reached = true; + } tracing::warn!( task_id = %task_id, reason = ?circuit_breaker.open_reason, + iteration_limit_reached = iteration_limit_reached, "Circuit breaker tripped, stopping autonomous loop" ); let msg = DaemonMessage::task_output( task_id, - format!("\n[Autonomous Loop] Circuit breaker tripped: {}\n", - circuit_breaker.open_reason.as_deref().unwrap_or("Unknown reason") - ), + format!("\n[Autonomous Loop] Circuit breaker tripped: {}\n", reason), false, ); let _ = self.ws_tx.send(msg).await; @@ -4506,16 +4545,20 @@ impl TaskManagerInner { let had_progress = output_bytes > 0; if !circuit_breaker.record_iteration(had_progress, None) { + // Circuit breaker tripped - check if it was due to max iterations + let reason = circuit_breaker.open_reason.as_deref().unwrap_or("Unknown reason"); + if reason.contains("Maximum iterations") { + iteration_limit_reached = true; + } tracing::warn!( task_id = %task_id, reason = ?circuit_breaker.open_reason, + iteration_limit_reached = iteration_limit_reached, "Circuit breaker tripped (no COMPLETION_GATE), stopping" ); let msg = DaemonMessage::task_output( task_id, - format!("\n[Autonomous Loop] Circuit breaker tripped: {}\n", - circuit_breaker.open_reason.as_deref().unwrap_or("Unknown reason") - ), + format!("\n[Autonomous Loop] Circuit breaker tripped: {}\n", reason), false, ); let _ = self.ws_tx.send(msg).await; @@ -4538,9 +4581,12 @@ impl TaskManagerInner { } } // end 'autonomous_loop - // Update state based on exit code + // Update state based on exit code and iteration limit let success = final_exit_code == 0; - let new_state = if success { + let new_state = if iteration_limit_reached { + // Task hit the max iteration limit - special state that allows resuming + TaskState::IterationLimitReached + } else if success { TaskState::Completed } else { TaskState::Failed @@ -4550,6 +4596,7 @@ impl TaskManagerInner { task_id = %task_id, exit_code = final_exit_code, success = success, + iteration_limit_reached = iteration_limit_reached, new_state = ?new_state, "Claude process exited, updating task state" ); @@ -4559,7 +4606,12 @@ impl TaskManagerInner { if let Some(task) = tasks.get_mut(&task_id) { task.state = new_state; task.completed_at = Some(Instant::now()); - if !success { + if iteration_limit_reached { + task.error = Some(format!( + "Task stopped after {} iterations (max: {}). Can be resumed with higher limit.", + iteration_count, effective_max_iterations + )); + } else if !success { task.error = Some(format!("Process exited with code {}", final_exit_code)); } } @@ -4621,17 +4673,39 @@ impl TaskManagerInner { ); let _ = self.ws_tx.send(msg).await; } else { - let error = if success { + let error = if iteration_limit_reached { + Some(format!( + "Iteration limit reached ({}/{}). Task can be resumed with higher limit.", + iteration_count, effective_max_iterations + )) + } else if success { None } else { Some(format!("Exit code: {}", final_exit_code)) }; - tracing::info!(task_id = %task_id, success = success, "Notifying server of task completion"); - let msg = DaemonMessage::task_complete(task_id, success, error); + // Mark iteration_limit_reached as successful for status purposes (not a failure) + // but send the specific status via send_status_change + let task_success = success || iteration_limit_reached; + tracing::info!( + task_id = %task_id, + success = task_success, + iteration_limit_reached = iteration_limit_reached, + "Notifying server of task completion" + ); + + if iteration_limit_reached { + // Send specific status change for iteration limit + self.send_status_change(task_id, "running", "iteration_limit_reached").await; + } + + // Send task complete message + let msg = DaemonMessage::task_complete(task_id, task_success, error); let _ = self.ws_tx.send(msg).await; - // Remove completed task from local database (no longer needs crash recovery) - self.remove_task_from_local_db(task_id); + // Only remove from local database if fully completed (not paused at limit) + if !iteration_limit_reached { + self.remove_task_from_local_db(task_id); + } } // Note: Worktrees are kept until explicitly deleted (per user preference) @@ -5098,6 +5172,7 @@ impl Clone for TaskManagerInner { contract_task_counts: self.contract_task_counts.clone(), checkpoint_patches: self.checkpoint_patches.clone(), local_db: self.local_db.clone(), + autonomous_loop_config: self.autonomous_loop_config.clone(), } } } diff --git a/makima/src/daemon/task/state.rs b/makima/src/daemon/task/state.rs index 7b59b62..ed7c177 100644 --- a/makima/src/daemon/task/state.rs +++ b/makima/src/daemon/task/state.rs @@ -21,6 +21,9 @@ pub enum TaskState { Failed, /// Task interrupted by user. Interrupted, + /// Task stopped due to reaching maximum iteration limit in autonomous loop mode. + /// Task can be resumed with a higher limit if needed. + IterationLimitReached, } impl TaskState { @@ -44,6 +47,7 @@ impl TaskState { | (Running, Completed) | (Running, Failed) | (Running, Interrupted) + | (Running, IterationLimitReached) // From Paused | (Paused, Running) | (Paused, Interrupted) @@ -59,7 +63,7 @@ impl TaskState { pub fn is_terminal(&self) -> bool { matches!( self, - TaskState::Completed | TaskState::Failed | TaskState::Interrupted + TaskState::Completed | TaskState::Failed | TaskState::Interrupted | TaskState::IterationLimitReached ) } @@ -91,6 +95,7 @@ impl TaskState { TaskState::Completed => "done", TaskState::Failed => "failed", TaskState::Interrupted => "interrupted", + TaskState::IterationLimitReached => "iteration_limit_reached", } } @@ -105,6 +110,7 @@ impl TaskState { "done" | "completed" => Some(TaskState::Completed), "failed" => Some(TaskState::Failed), "interrupted" => Some(TaskState::Interrupted), + "iteration_limit_reached" => Some(TaskState::IterationLimitReached), _ => None, } } diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs index 2e7caef..4ea0c5e 100644 --- a/makima/src/daemon/ws/protocol.rs +++ b/makima/src/daemon/ws/protocol.rs @@ -422,6 +422,10 @@ pub enum DaemonCommand { /// Commit SHA to apply the patch on top of. #[serde(rename = "patchBaseSha", default, skip_serializing_if = "Option::is_none")] patch_base_sha: Option<String>, + /// Maximum iterations for autonomous loop mode (None = use daemon default). + /// Task stops with "iteration_limit_reached" status when limit is hit. + #[serde(rename = "maxIterations", default, skip_serializing_if = "Option::is_none")] + max_iterations: Option<u32>, }, /// Pause a running task. diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 58f4da1..c71dec5 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -351,6 +351,9 @@ pub enum TaskStatus { Done, Failed, Merged, + /// Task stopped due to reaching maximum iteration limit in autonomous loop mode. + /// Task can be resumed with a higher limit if needed. + IterationLimitReached, } impl std::fmt::Display for TaskStatus { @@ -363,6 +366,7 @@ impl std::fmt::Display for TaskStatus { TaskStatus::Done => write!(f, "done"), TaskStatus::Failed => write!(f, "failed"), TaskStatus::Merged => write!(f, "merged"), + TaskStatus::IterationLimitReached => write!(f, "iteration_limit_reached"), } } } @@ -379,6 +383,7 @@ impl std::str::FromStr for TaskStatus { "done" => Ok(TaskStatus::Done), "failed" => Ok(TaskStatus::Failed), "merged" => Ok(TaskStatus::Merged), + "iteration_limit_reached" => Ok(TaskStatus::IterationLimitReached), _ => Err(format!("Unknown task status: {}", s)), } } @@ -531,6 +536,15 @@ pub struct Task { /// Standalone completed tasks can be dismissed by the user. #[serde(default)] pub hidden: bool, + + // Autonomous loop iteration tracking + /// Maximum iterations for autonomous loop mode (None = use daemon default). + /// Task stops with "iteration_limit_reached" status when limit is hit. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_iterations: Option<i32>, + /// Current iteration count in autonomous loop mode. + #[serde(default)] + pub iteration_count: i32, } impl Task { @@ -653,6 +667,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>, + /// Maximum iterations for autonomous loop mode (None = use daemon default). + /// Task stops with "iteration_limit_reached" status when limit is hit. + pub max_iterations: Option<i32>, } /// Request payload for updating a task @@ -684,6 +701,8 @@ pub struct UpdateTaskRequest { pub hidden: Option<bool>, /// Version for optimistic locking pub version: Option<i32>, + /// Update iteration count (for autonomous loop tracking) + pub iteration_count: Option<i32>, } /// Task with its subtasks for detail view diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index e2adb72..5740466 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1374,6 +1374,7 @@ async fn handle_contract_request( checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; match repository::create_task_for_owner(pool, owner_id, create_req).await { @@ -1470,6 +1471,7 @@ async fn handle_contract_request( checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; match repository::create_task_for_owner(pool, owner_id, create_req).await { @@ -1598,6 +1600,7 @@ async fn handle_contract_request( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: task.max_iterations.map(|i| i as u32), }; if let Err(e) = command_sender.send(command).await { @@ -2079,6 +2082,7 @@ async fn handle_contract_request( checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; match repository::create_task_for_owner(pool, owner_id, create_req).await { @@ -2595,6 +2599,7 @@ async fn handle_contract_request( checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; 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 462b385..b390cb3 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -298,6 +298,7 @@ pub async fn create_contract( merge_mode: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await { diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 3d05f35..342a8c2 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -691,6 +691,7 @@ pub async fn start_task( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: task.max_iterations.map(|i| i as u32), }; tracing::info!( @@ -743,6 +744,7 @@ pub async fn start_task( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: task.max_iterations.map(|i| i as u32), }; if state.send_daemon_command(alt_daemon_id, alt_command).await.is_ok() { @@ -1147,6 +1149,7 @@ pub async fn send_message( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: updated_task.max_iterations.map(|i| i as u32), }; if state.send_daemon_command(new_daemon_id, spawn_cmd).await.is_ok() { @@ -2225,6 +2228,7 @@ pub async fn reassign_task( checkpoint_sha: task.last_checkpoint_sha.clone(), branched_from_task_id: None, conversation_history: None, + max_iterations: task.max_iterations, }; let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await { @@ -2312,6 +2316,7 @@ pub async fn reassign_task( conversation_history: None, patch_data, patch_base_sha, + max_iterations: task.max_iterations.map(|i| i as u32), }; tracing::info!( @@ -2639,6 +2644,7 @@ pub async fn continue_task( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: task.max_iterations.map(|i| i as u32), }; tracing::info!( @@ -2974,6 +2980,7 @@ pub async fn fork_task( checkpoint_sha: Some(checkpoint.commit_sha.clone()), branched_from_task_id: None, conversation_history: None, + max_iterations: task.max_iterations, }; let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await { @@ -3131,6 +3138,7 @@ pub async fn resume_from_checkpoint( checkpoint_sha: Some(checkpoint.commit_sha.clone()), branched_from_task_id: None, conversation_history: None, + max_iterations: task.max_iterations, }; let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await { @@ -3466,6 +3474,7 @@ pub async fn branch_task( checkpoint_sha: None, branched_from_task_id: Some(source_task_id), conversation_history, + max_iterations: source_task.max_iterations, }; let task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await { @@ -3535,6 +3544,7 @@ pub async fn branch_task( conversation_history: updated_task.conversation_state.clone(), patch_data: None, patch_base_sha: None, + max_iterations: updated_task.max_iterations.map(|i| i as u32), }; if let Err(e) = state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs index 1ff0724..72aa2fd 100644 --- a/makima/src/server/handlers/mesh_chat.rs +++ b/makima/src/server/handlers/mesh_chat.rs @@ -1020,6 +1020,7 @@ async fn handle_mesh_request( checkpoint_sha: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; match repository::create_task_for_owner(pool, owner_id, create_req).await { @@ -1153,6 +1154,7 @@ async fn handle_mesh_request( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: task.max_iterations.map(|i| i as u32), }; match state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 1b5e376..196ba0f 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -399,6 +399,7 @@ pub async fn try_start_pending_task( conversation_history: None, patch_data, patch_base_sha, + max_iterations: updated_task.max_iterations.map(|i| i as u32), }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -614,6 +615,7 @@ pub async fn spawn_task( copy_files: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; // Create task in DB @@ -701,6 +703,7 @@ pub async fn spawn_task( conversation_history: None, patch_data: None, patch_base_sha: None, + max_iterations: updated_task.max_iterations.map(|i| i as u32), }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -2074,6 +2077,7 @@ pub async fn resume_supervisor( conversation_history: Some(supervisor_state.conversation_history.clone()), // Fallback if worktree missing patch_data, patch_base_sha, + max_iterations: supervisor_task.max_iterations.map(|i| i as u32), }; if let Err(e) = state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs index 3b71eca..89c5688 100644 --- a/makima/src/server/handlers/transcript_analysis.rs +++ b/makima/src/server/handlers/transcript_analysis.rs @@ -366,6 +366,7 @@ pub async fn create_contract_from_analysis( merge_mode: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await { @@ -535,6 +536,7 @@ pub async fn update_contract_from_analysis( merge_mode: None, branched_from_task_id: None, conversation_history: None, + max_iterations: None, }; if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await { diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index 5b75281..1c22088 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -223,6 +223,9 @@ pub enum DaemonCommand { /// Commit SHA to apply the patch on top of #[serde(rename = "patchBaseSha", default, skip_serializing_if = "Option::is_none")] patch_base_sha: Option<String>, + /// Maximum iterations for autonomous loop mode + #[serde(rename = "maxIterations", default, skip_serializing_if = "Option::is_none")] + max_iterations: Option<u32>, }, /// Pause a running task PauseTask { |
