From e2dbc7be6bead36c08cdfbf07c279089deeff01c Mon Sep 17 00:00:00 2001 From: Marco Barisione Date: Wed, 25 Feb 2026 13:31:58 +0000 Subject: [PATCH 1/4] Explain: fix void type comparison in `_reverse_into_target_function` `func.type.target` returns a `gdb.Type` object, not a type code integer. --- explain/explain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explain/explain.py b/explain/explain.py index fa84fb9..e7c5ded 100644 --- a/explain/explain.py +++ b/explain/explain.py @@ -595,7 +595,7 @@ def _reverse_into_target_function(self, target_fn: str) -> str: assert gdb.selected_frame().name() == target_fn func = gdb.selected_frame().function() - if func and func.type.target() != gdb.TYPE_CODE_VOID: + if func and func.type.target().code != gdb.TYPE_CODE_VOID: # Step further back to ensure we're at the return statement. with gdbutils.temporary_parameter("listsize", 1): while "return" not in gdbutils.execute_to_string("list"): From 3ed543577ac5a0cfd9150826cad632ecfafb765f Mon Sep 17 00:00:00 2001 From: Marco Barisione Date: Wed, 25 Feb 2026 13:31:59 +0000 Subject: [PATCH 2/4] Explain: handle single-line functions in `_reverse_into_target_function` When searching for the `return` statement, if we hit the function start breakpoint again it means the function body is a single line; break out of the loop instead of asserting. --- explain/explain.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/explain/explain.py b/explain/explain.py index e7c5ded..324f18d 100644 --- a/explain/explain.py +++ b/explain/explain.py @@ -599,16 +599,15 @@ def _reverse_into_target_function(self, target_fn: str) -> str: # Step further back to ensure we're at the return statement. with gdbutils.temporary_parameter("listsize", 1): while "return" not in gdbutils.execute_to_string("list"): + if target_start_bp.hit_count > 1: + # We've gone back to the start of the function without finding + # a return statement. This can happen with single-line functions. + break self.udb.execution.reverse_next(cmd="reverse-next") # Check we're still in the function we intended. assert gdb.selected_frame().name() == target_fn - # And that we've not gone back further than planned. - assert ( - target_start_bp.hit_count == 1 - ), "Unexpectedly reached the start of the target function." - if LOG_LEVEL == "DEBUG": print(f"_reverse_into_target_function internal messages:\n{collector.output}") From c27c1aadaee67e4b518c85e17085259b6e5505bf Mon Sep 17 00:00:00 2001 From: Marco Barisione Date: Wed, 25 Feb 2026 13:32:00 +0000 Subject: [PATCH 3/4] Explain: add revert-on-failure behaviour to `ugo_sender` If switching to the sending process fails, restore the debugger to its original position so the failed tool call does not leave the session in an unexpected state. --- explain/explain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/explain/explain.py b/explain/explain.py index 324f18d..f84da3a 100644 --- a/explain/explain.py +++ b/explain/explain.py @@ -453,6 +453,7 @@ def _call_handler(_): @report @source_context @collect_output + @revert_time_on_failure @chain_of_thought def tool_ugo_sender(self) -> None: """ From 2e073e3181cecf9dad731f8a9148032322829e74 Mon Sep 17 00:00:00 2001 From: Marco Barisione Date: Wed, 25 Feb 2026 13:32:01 +0000 Subject: [PATCH 4/4] Explain: revert debugger time on `reverse_finish` failure Previously, `reverse_finish` only restored the selected frame on failure. Just reverting the frame should work considering what `reverse_finish`, but restoring the full bookmarked time is safer, consistent with other tools and restores the frame anyway. --- explain/explain.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/explain/explain.py b/explain/explain.py index f84da3a..88ebbfa 100644 --- a/explain/explain.py +++ b/explain/explain.py @@ -490,6 +490,7 @@ def tool_reverse_next(self) -> None: @report @source_context @collect_output + @revert_time_on_failure @chain_of_thought def tool_reverse_finish(self, target_fn: str) -> None: """ @@ -500,29 +501,24 @@ def tool_reverse_finish(self, target_fn: str) -> None: which will improve performance. On success it will pop at least one stack frame, even in recursive calls. On failure it will - return to the originally-selected stack frame. + return to the original point in time. Params: target_fn: the function you want to reverse-finish back to. This must be present in the current backtrace or the command will fail. """ - orig_frame = gdbutils.selected_frame() - try: - frame = orig_frame.older() - while frame and frame.name() != target_fn: - frame = frame.older() + frame = gdbutils.selected_frame().older() + while frame and frame.name() != target_fn: + frame = frame.older() - if not frame: - raise Exception("No such frame in current backtrace.") + if not frame: + raise Exception("No such frame in current backtrace.") - # Finish out into the specified frame. - frame.newer().select() - self.udb.execution.reverse_finish(cmd="reverse-finish") + # Finish out into the specified frame. + frame.newer().select() + self.udb.execution.reverse_finish(cmd="reverse-finish") - assert gdbutils.selected_frame().name() == target_fn - except: - orig_frame.select() - raise + assert gdbutils.selected_frame().name() == target_fn def _reverse_into_target_function(self, target_fn: str) -> str: """