summaryrefslogtreecommitdiff
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
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>
-rw-r--r--makima/src/bin/makima.rs1
-rw-r--r--makima/src/daemon/config.rs56
-rw-r--r--makima/src/daemon/task/manager.rs111
-rw-r--r--makima/src/daemon/task/state.rs8
-rw-r--r--makima/src/daemon/ws/protocol.rs4
-rw-r--r--makima/src/db/models.rs19
-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
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 {