Skip to content

fix(memory): paginate list_messages by converted message count, not raw event count#347

Open
Hweinstock wants to merge 1 commit intoaws:mainfrom
Hweinstock:fix/list-messages-pagination
Open

fix(memory): paginate list_messages by converted message count, not raw event count#347
Hweinstock wants to merge 1 commit intoaws:mainfrom
Hweinstock:fix/list-messages-pagination

Conversation

@Hweinstock
Copy link
Contributor

@Hweinstock Hweinstock commented Mar 16, 2026

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

  • 1 new unit test: test_list_messages_with_limit_skips_state_events — mocks 2 pages with mixed state + conversational events, asserts limit=5 returns exactly 5 messages
  • 1 new unit test: test_list_messages_with_limit_returns_fewer_when_not_enough — verifies correct behavior when fewer messages exist than requested
  • 1 new integration test: test_list_messages_limit_excludes_state_events — creates a real session with multiple agent turns, asserts limit=4 returns exactly 4 messages

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov-commenter
Copy link

codecov-commenter commented Mar 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@f8710fa). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #347   +/-   ##
=======================================
  Coverage        ?   90.97%           
=======================================
  Files           ?       44           
  Lines           ?     4087           
  Branches        ?      626           
=======================================
  Hits            ?     3718           
  Misses          ?      203           
  Partials        ?      166           
Flag Coverage Δ
unittests 90.97% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Hweinstock Hweinstock force-pushed the fix/list-messages-pagination branch from 2c87462 to 29044df Compare March 16, 2026 18:03
@Hweinstock Hweinstock force-pushed the fix/list-messages-pagination branch from 29044df to 3489f1b Compare March 16, 2026 18:06
@Hweinstock Hweinstock marked this pull request as ready for review March 16, 2026 18:20
@Hweinstock Hweinstock requested a review from a team March 16, 2026 18:20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants