fix(engine): transition thread to Waiting on NeedApproval

The orchestrator Python returned {"outcome": "need_approval"} without
calling __transition_to__("waiting"), leaving the thread in Running
state. When the user later approved/denied, resume_thread rejected it
with "thread is not resumable from Running".

- Add __transition_to__("waiting", "approval needed") in both code-step
  and action-call approval paths in default.py
- Add Rust safety net in loop_engine.rs: if orchestrator returns
  NeedApproval but thread isn't Waiting, force the transition

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-28 14:20:16 -07:00
co-authored by Claude Opus 4.6
parent 62ea08ac5e
commit 67d5a47333
2 changed files with 18 additions and 0 deletions
@@ -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,
@@ -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?;