fix(memory): paginate list_messages by converted message count, not raw event count#347
fix(memory): paginate list_messages by converted message count, not raw event count#347Hweinstock wants to merge 1 commit intoaws:mainfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #347 +/- ##
=======================================
Coverage ? 90.97%
=======================================
Files ? 44
Lines ? 4087
Branches ? 626
=======================================
Hits ? 3718
Misses ? 203
Partials ? 166
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
2c87462 to
29044df
Compare
29044df to
3489f1b
Compare
There was a problem hiding this comment.
This test now passes for the wrong reason. It mocks mock_memory_client.list_events.side_effect = Exception("API Error"), but the production code now calls self.memory_client.gmdp_client.list_events(...) — a different attribute. The intended exception is never raised.
What actually happens: gmdp_client is a Mock(), so .list_events() returns a Mock, .get("events", []) returns another Mock, and all_items.extend(mock_obj) raises TypeError: 'Mock' object is not iterable, which the broad except Exception catches. The assertion len(messages) == 0 passes, but via the wrong error path.
Fix: session_manager.memory_client.gmdp_client.list_events.side_effect = Exception("API Error")
There was a problem hiding this comment.
Good catch! Let me update that to the proper client.
| target = (limit + offset) if limit else MAX_FETCH_ALL_RESULTS | ||
|
|
||
| def fetch_page(params: dict) -> tuple[list, str | None]: | ||
| response = self.memory_client.gmdp_client.list_events(**params) |
There was a problem hiding this comment.
This bypasses MemoryClient.list_events and calls gmdp_client.list_events directly. MemoryClient.list_events wraps the raw API with error handling (catches ClientError), parameter normalization, and filter construction — none of that applies here. It also means the session manager now has two different data-plane access patterns: every other method goes through MemoryClient, but list_messages reaches through to the internal client. If someone later adds logging or error normalization to MemoryClient.list_events, this code path silently misses it.
Recommendation: Add a paginated variant to MemoryClient — e.g., list_events_page(memory_id, actor_id, session_id, max_results, next_token) -> (events, next_token) — that wraps the raw API call with the same error handling. Then fetch_page calls that instead of reaching through to gmdp_client.
There was a problem hiding this comment.
The challenge here is that the list_events wrapper in MemoryClient already paginates, but does so in a way that is incompatible with this fix (still counts agent state towards max results).
I thought about trying to generalize the existing MemoryClient method, but doing so in a backwards-compatible way is going to be difficult.
For those reasons, I chose to drop down to the raw client to control the pagination myself.
3489f1b to
9afab4f
Compare
Issue #, if available: #346
Description of changes:
Problem
After #244, state events (session/agent blobs) share the same actorId as conversational events, distinguished only by metadata. list_messages passes
max_results to list_events, which counts all raw events — including state events that the converter discards. When a caller passes limit, they can receive fewer messages than requested because state events consume
slots in the result set.
Solution
Introduce a reusable paginate_for_n_results utility in _utils/pagination.py that paginates an arbitrary API, converting accumulated results after each page, and stops when enough converted items are collected or
pages are exhausted. list_messages now uses this utility to paginate gmdp_client.list_events directly, counting converted messages toward the limit instead of raw events.
Testing
test_list_messages_with_limit_skips_state_events— mocks 2 pages with mixed state + conversational events, asserts limit=5 returns exactly 5 messagestest_list_messages_with_limit_returns_fewer_when_not_enough— verifies correct behavior when fewer messages exist than requestedtest_list_messages_limit_excludes_state_events— creates a real session with multiple agent turns, asserts limit=4 returns exactly 4 messagesBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.