From cb2aa9a73163ce392d7c3f1dd81888b039312a67 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 24 Jan 2026 00:52:28 +0000 Subject: feat: Add maximum iterations limit for autonomous loop mode Adds configurable iteration limits to prevent runaway autonomous loops and provide predictable behavior, inspired by Ralph's design patterns. Changes: - Add AutonomousLoopConfig to daemon config with: - default_max_iterations: 10 (default for new tasks) - hard_limit: 50 (absolute maximum that cannot be exceeded) - no_change_threshold: 3 (consecutive runs without progress) - same_error_threshold: 5 (consecutive runs with same error) - Add max_iterations and iteration_count fields to Task model - Add iteration_limit_reached status to TaskStatus enum - Pass max_iterations through DaemonCommand::SpawnTask - Apply limits in CircuitBreaker during autonomous loop execution When a task hits the iteration limit: - Task status is set to "iteration_limit_reached" (not "failed") - Clear message is logged about hitting the limit - Task can be resumed with a higher limit if needed Co-Authored-By: Claude Opus 4.5 --- makima/src/server/handlers/contract_chat.rs | 5 +++++ makima/src/server/handlers/contracts.rs | 1 + makima/src/server/handlers/mesh.rs | 10 ++++++++++ makima/src/server/handlers/mesh_chat.rs | 2 ++ makima/src/server/handlers/mesh_supervisor.rs | 4 ++++ makima/src/server/handlers/transcript_analysis.rs | 2 ++ makima/src/server/state.rs | 3 +++ 7 files changed, 27 insertions(+) (limited to 'makima/src/server') 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, + /// Maximum iterations for autonomous loop mode + #[serde(rename = "maxIterations", default, skip_serializing_if = "Option::is_none")] + max_iterations: Option, }, /// Pause a running task PauseTask { -- cgit v1.2.3