From 96cae4768deede71c65876069d9b45f5e7821796 Mon Sep 17 00:00:00 2001 From: ninglinLiu Date: Fri, 30 Oct 2026 13:28:59 +0800 Subject: [PATCH] fix(events): drop rewound turns when rewind target is outside the window _apply_rewinds only jumped when the rewind target invocation was present in the loaded list. A GetSessionConfig.num_recent_events / after_timestamp window that starts after the target left the rewind marker without a match, so later (rewound) turns leaked back into the LLM prompt. When the target is missing, stop the backward walk so every earlier event in the list stays out of the active history. Fixes #7859 --- src/google/adk/events/_rewind_events.py | 8 ++++++- tests/unittests/events/test_rewind_events.py | 22 ++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/google/adk/events/_rewind_events.py b/src/google/adk/events/_rewind_events.py index 0ef2d9037c8..d324a3f3687 100644 --- a/src/google/adk/events/_rewind_events.py +++ b/src/google/adk/events/_rewind_events.py @@ -64,7 +64,13 @@ def _apply_rewinds(events: list[Event]) -> list[Event]: rewind_idx = first_idx_by_inv.get( event.actions.rewind_before_invocation_id ) - if rewind_idx is not None and rewind_idx < i: + if rewind_idx is None: + # Target is not in `events` — either an unknown id, or the list was + # truncated by GetSessionConfig.num_recent_events / after_timestamp + # so the target fell outside the window. Everything earlier in this + # list belongs to the rewound span; stop so those events stay out. + break + if rewind_idx < i: i = rewind_idx else: kept.append(event) diff --git a/tests/unittests/events/test_rewind_events.py b/tests/unittests/events/test_rewind_events.py index 1fd98629665..1f2ee0d0b0a 100644 --- a/tests/unittests/events/test_rewind_events.py +++ b/tests/unittests/events/test_rewind_events.py @@ -108,10 +108,28 @@ def test_get_active_events_rewind_target_not_found(): ) events = [event_a, rewind_event] + # Target missing from the list (unknown id or truncated window): drop + # every earlier event so rewound turns cannot leak into a prompt. + assert get_active_events(events) == [] + + +def test_get_active_events_rewind_target_outside_window(): + """num_recent_events can omit the rewind target; later turns must still drop.""" + event_b = _make_event("inv-2", "user") + event_b_ok = _make_event("inv-2", "agent") + rewind_event = _make_event( + "inv-rewind", "user", rewind_before_invocation_id="inv-1" + ) + event_c = _make_event("inv-3", "user") + event_c_ok = _make_event("inv-3", "agent") + + # Window starts at inv-2; inv-1 (the rewind target) is not loaded. + events = [event_b, event_b_ok, rewind_event, event_c, event_c_ok] result = get_active_events(events) - assert len(result) == 1 - assert result[0] is event_a + assert len(result) == 2 + assert result[0] is event_c + assert result[1] is event_c_ok def test_get_active_events_preserves_chronological_order():