diff --git a/crates/ironclaw_engine/orchestrator/default.py b/crates/ironclaw_engine/orchestrator/default.py index ae1f7a15..30780dd8 100644 --- a/crates/ironclaw_engine/orchestrator/default.py +++ b/crates/ironclaw_engine/orchestrator/default.py @@ -339,6 +339,7 @@ def run_loop(context, goal, actions, state, config): "nudge_count": nudge_count, "consecutive_errors": consecutive_errors, }) + __transition_to__("waiting", "approval needed") return { "outcome": "need_approval", "action_name": approval.get("action_name", ""), @@ -382,6 +383,7 @@ def run_loop(context, goal, actions, state, config): "nudge_count": nudge_count, "consecutive_errors": consecutive_errors, }) + __transition_to__("waiting", "approval needed") return { "outcome": "need_approval", "action_name": name, diff --git a/crates/ironclaw_engine/src/executor/loop_engine.rs b/crates/ironclaw_engine/src/executor/loop_engine.rs index 9240d6a8..c62fcdd7 100644 --- a/crates/ironclaw_engine/src/executor/loop_engine.rs +++ b/crates/ironclaw_engine/src/executor/loop_engine.rs @@ -305,6 +305,22 @@ impl ExecutionLoop { .await; } let _ = &orch_result.tokens_used; + + // Safety net: if the orchestrator returned NeedApproval but + // didn't transition to Waiting, do it now so resume_thread works. + if matches!(orch_result.outcome, ThreadOutcome::NeedApproval { .. }) + && self.thread.state != ThreadState::Waiting + { + debug!( + thread_id = %self.thread.id, + state = ?self.thread.state, + "orchestrator returned NeedApproval without transitioning to Waiting" + ); + let _ = self + .thread + .transition_to(ThreadState::Waiting, Some("approval needed".into())); + } + self.clear_runtime_checkpoint(); self.persist_runtime_state(None, &mut persisted_event_count) .await?;