From b080891efa9a5c22389b6bc67d80dcec827b3c59 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Fri, 15 May 2026 17:43:07 -0500 Subject: [PATCH] fix(tui): count loop guard blocks as failures (#1658) --- CHANGELOG.md | 8 +++++++- crates/tui/CHANGELOG.md | 8 +++++++- crates/tui/src/core/engine/turn_loop.rs | 27 +++++++++++++++++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c7bac5..dc2e9ceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 legacy Windows console hosts now automatically enable low-motion rendering, disable fancy animations, and resolve `synchronized_output = "auto"` to off so streaming redraws do not overlap or visibly flicker (#1590). +- **LoopGuard blocks now count as failed tool calls.** Identical tool-call + blocks now return a failed tool result instead of a success, so repeated + blocked checklist/tool retries can trip the existing failure warning and halt + path instead of spinning indefinitely (#1574). ### Thanks @@ -59,7 +63,9 @@ picker catalog work harvested from #1201. Thanks to **[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows composer scroll fix harvested from #1578, and **WuMing ([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows -PowerShell flicker fix harvested from #1591. +PowerShell flicker fix harvested from #1591. Thanks to +**[@maker316](https://github.com/maker316)** for the LoopGuard/checklist loop +report in #1574. ## [0.8.37] - 2026-05-14 diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 66c7bac5..dc2e9ceb 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -47,6 +47,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 legacy Windows console hosts now automatically enable low-motion rendering, disable fancy animations, and resolve `synchronized_output = "auto"` to off so streaming redraws do not overlap or visibly flicker (#1590). +- **LoopGuard blocks now count as failed tool calls.** Identical tool-call + blocks now return a failed tool result instead of a success, so repeated + blocked checklist/tool retries can trip the existing failure warning and halt + path instead of spinning indefinitely (#1574). ### Thanks @@ -59,7 +63,9 @@ picker catalog work harvested from #1201. Thanks to **[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows composer scroll fix harvested from #1578, and **WuMing ([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows -PowerShell flicker fix harvested from #1591. +PowerShell flicker fix harvested from #1591. Thanks to +**[@maker316](https://github.com/maker316)** for the LoopGuard/checklist loop +report in #1574. ## [0.8.37] - 2026-05-14 diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index bfcbf3e2..43bae40a 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -7,6 +7,10 @@ use super::*; +fn loop_guard_block_tool_result(message: String) -> ToolResult { + ToolResult::error(message).with_metadata(json!({"loop_guard": "identical_tool_call"})) +} + impl Engine { pub(super) async fn handle_deepseek_turn( &mut self, @@ -1214,10 +1218,7 @@ impl Engine { loop_guard.record_attempt(&tool_name, &tool_input) { crate::logging::warn(message.clone()); - guard_result = Some( - ToolResult::success(message) - .with_metadata(json!({"loop_guard": "identical_tool_call"})), - ); + guard_result = Some(loop_guard_block_tool_result(message)); } plans.push(ToolExecutionPlan { @@ -2023,6 +2024,24 @@ mod tests { assert!(!should_hold_turn_for_subagents(0, 0)); } + #[test] + fn loop_guard_block_tool_result_counts_as_failure() { + let result = loop_guard_block_tool_result("Blocked: repeated call".to_string()); + + assert!( + !result.success, + "LoopGuard blocks must count as tool failures so repeated blocked calls can trip halt handling" + ); + assert_eq!( + result + .metadata + .as_ref() + .and_then(|m| m.get("loop_guard")) + .and_then(|v| v.as_str()), + Some("identical_tool_call") + ); + } + #[test] fn resolve_auto_effort_ignores_stored_turn_metadata() { let messages = vec![Message {