summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-24 00:52:28 +0000
committersoryu <soryu@soryu.co>2026-01-24 00:52:28 +0000
commitcb2aa9a73163ce392d7c3f1dd81888b039312a67 (patch)
tree4b124f0f8ab3762140fa8b7ed2e0370e479fedae /makima/src/server
parent579c983d3efb8f1414ffb45b9e031f741cce5f76 (diff)
downloadsoryu-cb2aa9a73163ce392d7c3f1dd81888b039312a67.tar.gz
soryu-cb2aa9a73163ce392d7c3f1dd81888b039312a67.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/contract_chat.rs5
-rw-r--r--makima/src/server/handlers/contracts.rs1
-rw-r--r--makima/src/server/handlers/mesh.rs10
-rw-r--r--makima/src/server/handlers/mesh_chat.rs2
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs4
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs2
-rw-r--r--makima/src/server/state.rs3
7 files changed, 27 insertions, 0 deletions
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 {