From 5a479a3bc9ed543be28c54b4f7e19645bbae5e0f Mon Sep 17 00:00:00 2001 From: Jakob Berg Date: Tue, 16 Sep 2025 17:15:16 -0400 Subject: [PATCH 1/6] fix: add monotonic, microsecond clock for create_event --- .../integrations/strands/session_manager.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 3cbe3aaa..cbcdcf31 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -68,6 +68,24 @@ def __init__( self.memory_client = MemoryClient(region_name=region_name) session = boto_session or boto3.Session(region_name=region_name) self.has_existing_agent = False + self._last_timestamp = None + self._sequence_counter = 0 + + def _get_monotonic_timestamp(self) -> datetime: + """Generate a monotonically increasing timestamp with microsecond precision.""" + current = datetime.now(timezone.utc) + + if self._last_timestamp is None or current > self._last_timestamp: + self._last_timestamp = current + self._sequence_counter = 0 + else: + # Same or earlier time - increment sequence and add microseconds + self._sequence_counter += 1 + self._last_timestamp = self._last_timestamp.replace( + microsecond=min(999999, self._last_timestamp.microsecond + self._sequence_counter) + ) + + return self._last_timestamp # Override the clients if custom boto session or config is provided # Add strands-agents to the request user agent @@ -149,7 +167,7 @@ def create_session(self, session: Session, **kwargs: Any) -> Session: payload=[ {"blob": json.dumps(session.to_dict())}, ], - eventTimestamp=datetime.now(timezone.utc), + eventTimestamp=self._get_monotonic_timestamp(), ) logger.info("Created session: %s with event: %s", session.session_id, event.get("event", {}).get("eventId")) return session @@ -220,7 +238,7 @@ def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: A payload=[ {"blob": json.dumps(session_agent.to_dict())}, ], - eventTimestamp=datetime.now(timezone.utc), + eventTimestamp=self._get_monotonic_timestamp(), ) logger.info( "Created agent: %s in session: %s with event %s", @@ -325,7 +343,7 @@ def create_message( actor_id=self.config.actor_id, session_id=session_id, messages=messages, - event_timestamp=datetime.fromisoformat(session_message.created_at.replace("Z", "+00:00")), + event_timestamp=self._get_monotonic_timestamp(), ) else: event = self.memory_client.gmdp_client.create_event( @@ -335,7 +353,7 @@ def create_message( payload=[ {"blob": json.dumps(messages[0])}, ], - eventTimestamp=datetime.fromisoformat(session_message.created_at.replace("Z", "+00:00")), + eventTimestamp=self._get_monotonic_timestamp(), ) logger.debug("Created event: %s for message: %s", event.get("eventId"), session_message.message_id) return event From ed7a4c48e19af35e015dfcde60b2fe2d8324ed5b Mon Sep 17 00:00:00 2001 From: Jakob Berg Date: Tue, 16 Sep 2025 17:16:51 -0400 Subject: [PATCH 2/6] fix: make monotonic timestamp have a lock --- .../integrations/strands/session_manager.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index cbcdcf31..d64893b3 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -2,6 +2,7 @@ import json import logging +import threading from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Optional @@ -70,22 +71,24 @@ def __init__( self.has_existing_agent = False self._last_timestamp = None self._sequence_counter = 0 + self._timestamp_lock = threading.Lock() def _get_monotonic_timestamp(self) -> datetime: """Generate a monotonically increasing timestamp with microsecond precision.""" - current = datetime.now(timezone.utc) - - if self._last_timestamp is None or current > self._last_timestamp: - self._last_timestamp = current - self._sequence_counter = 0 - else: - # Same or earlier time - increment sequence and add microseconds - self._sequence_counter += 1 - self._last_timestamp = self._last_timestamp.replace( - microsecond=min(999999, self._last_timestamp.microsecond + self._sequence_counter) - ) + with self._timestamp_lock: + current = datetime.now(timezone.utc) - return self._last_timestamp + if self._last_timestamp is None or current > self._last_timestamp: + self._last_timestamp = current + self._sequence_counter = 0 + else: + # Same or earlier time - increment sequence and add microseconds + self._sequence_counter += 1 + self._last_timestamp = self._last_timestamp.replace( + microsecond=min(999999, self._last_timestamp.microsecond + self._sequence_counter) + ) + + return self._last_timestamp # Override the clients if custom boto session or config is provided # Add strands-agents to the request user agent From b7fc90577bf85ce50fba1290f6de2c994a15b832 Mon Sep 17 00:00:00 2001 From: Jakob Berg Date: Tue, 16 Sep 2025 17:29:18 -0400 Subject: [PATCH 3/6] chore: wrapper method for create_event --- .../integrations/strands/session_manager.py | 14 +-- .../test_agentcore_memory_session_manager.py | 92 +++++++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index d64893b3..5cd575fb 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -90,6 +90,11 @@ def _get_monotonic_timestamp(self) -> datetime: return self._last_timestamp + def _create_event_with_monotonic_timestamp(self, **kwargs): + """Create event with guaranteed monotonic timestamp.""" + kwargs['eventTimestamp'] = self._get_monotonic_timestamp() + return self.memory_client.gmdp_client.create_event(**kwargs) + # Override the clients if custom boto session or config is provided # Add strands-agents to the request user agent if boto_client_config: @@ -163,14 +168,13 @@ def create_session(self, session: Session, **kwargs: Any) -> Session: if session.session_id != self.config.session_id: raise SessionException(f"Session ID mismatch: expected {self.config.session_id}, got {session.session_id}") - event = self.memory_client.gmdp_client.create_event( + event = self._create_event_with_monotonic_timestamp( memoryId=self.config.memory_id, actorId=self._get_full_session_id(session.session_id), sessionId=self.session_id, payload=[ {"blob": json.dumps(session.to_dict())}, ], - eventTimestamp=self._get_monotonic_timestamp(), ) logger.info("Created session: %s with event: %s", session.session_id, event.get("event", {}).get("eventId")) return session @@ -234,14 +238,13 @@ def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: A if session_id != self.config.session_id: raise SessionException(f"Session ID mismatch: expected {self.config.session_id}, got {session_id}") - event = self.memory_client.gmdp_client.create_event( + event = self._create_event_with_monotonic_timestamp( memoryId=self.config.memory_id, actorId=self._get_full_agent_id(session_agent.agent_id), sessionId=self.session_id, payload=[ {"blob": json.dumps(session_agent.to_dict())}, ], - eventTimestamp=self._get_monotonic_timestamp(), ) logger.info( "Created agent: %s in session: %s with event %s", @@ -349,14 +352,13 @@ def create_message( event_timestamp=self._get_monotonic_timestamp(), ) else: - event = self.memory_client.gmdp_client.create_event( + event = self._create_event_with_monotonic_timestamp( memoryId=self.config.memory_id, actorId=self.config.actor_id, sessionId=session_id, payload=[ {"blob": json.dumps(messages[0])}, ], - eventTimestamp=self._get_monotonic_timestamp(), ) logger.debug("Created event: %s for message: %s", event.get("eventId"), session_message.message_id) return event diff --git a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py index 46b68fe1..09d984cf 100644 --- a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py +++ b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py @@ -1,5 +1,6 @@ """Tests for AgentCoreMemorySessionManager.""" +import threading from unittest.mock import Mock, patch import pytest @@ -1047,3 +1048,94 @@ def test_retrieve_customer_context_exception(self, agentcore_config_with_retriev # Should not raise exception, just log error manager.retrieve_customer_context(event) + + +class TestMonotonicTimestamp: + """Test monotonic timestamp generation.""" + + def test_monotonic_timestamps_sequential(self, session_manager): + """Test that sequential calls produce increasing timestamps.""" + timestamps = [] + for _ in range(10): + timestamps.append(session_manager._get_monotonic_timestamp()) + + # Verify all timestamps are strictly increasing + for i in range(1, len(timestamps)): + assert timestamps[i] > timestamps[i-1] + + def test_monotonic_timestamps_concurrent(self, session_manager): + """Test that concurrent calls produce unique increasing timestamps.""" + timestamps = [] + lock = threading.Lock() + + def get_timestamp(): + ts = session_manager._get_monotonic_timestamp() + with lock: + timestamps.append(ts) + + # Create multiple threads + threads = [] + for _ in range(20): + thread = threading.Thread(target=get_timestamp) + threads.append(thread) + + # Start all threads + for thread in threads: + thread.start() + + # Wait for completion + for thread in threads: + thread.join() + + # Sort timestamps and verify uniqueness and ordering + timestamps.sort() + assert len(timestamps) == 20 + assert len(set(timestamps)) == 20 # All unique + + # Verify strictly increasing + for i in range(1, len(timestamps)): + assert timestamps[i] > timestamps[i-1] + + def test_microsecond_precision(self, session_manager): + """Test that rapid calls get microsecond-level differentiation.""" + # Get many timestamps rapidly + timestamps = [] + for _ in range(100): + timestamps.append(session_manager._get_monotonic_timestamp()) + + # Should have microsecond differences + for i in range(1, len(timestamps)): + diff = timestamps[i] - timestamps[i-1] + assert diff.total_seconds() > 0 + # Should be small differences (microseconds) + assert diff.total_seconds() < 0.001 + + def test_create_event_wrapper_uses_monotonic_timestamp(self, session_manager): + """Test that the create_event wrapper uses monotonic timestamps.""" + # Mock the underlying create_event method + session_manager.memory_client.gmdp_client.create_event = Mock(return_value={"eventId": "test-123"}) + + # Call the wrapper multiple times + session_manager._create_event_with_monotonic_timestamp( + memoryId="test-memory", + actorId="test-actor", + sessionId="test-session", + payload=[{"blob": "test"}] + ) + + session_manager._create_event_with_monotonic_timestamp( + memoryId="test-memory", + actorId="test-actor", + sessionId="test-session", + payload=[{"blob": "test2"}] + ) + + # Verify create_event was called twice + assert session_manager.memory_client.gmdp_client.create_event.call_count == 2 + + # Get the timestamps from both calls + call1_timestamp = session_manager.memory_client.gmdp_client.create_event.call_args_list[0][1]['eventTimestamp'] + call2_timestamp = session_manager.memory_client.gmdp_client.create_event.call_args_list[1][1]['eventTimestamp'] + + # Verify timestamps are monotonically increasing + assert call2_timestamp > call1_timestamp From cbc4190b49361f282a67b423da018e5667fd5002 Mon Sep 17 00:00:00 2001 From: Jakob Berg Date: Tue, 16 Sep 2025 18:13:52 -0400 Subject: [PATCH 4/6] fix: dont let LLM mess up code --- .../integrations/strands/session_manager.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 5cd575fb..7656ee57 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -73,6 +73,27 @@ def __init__( self._sequence_counter = 0 self._timestamp_lock = threading.Lock() + # Override the clients if custom boto session or config is provided + # Add strands-agents to the request user agent + if boto_client_config: + existing_user_agent = getattr(boto_client_config, "user_agent_extra", None) + if existing_user_agent: + new_user_agent = f"{existing_user_agent} strands-agents" + else: + new_user_agent = "strands-agents" + client_config = boto_client_config.merge(BotocoreConfig(user_agent_extra=new_user_agent)) + else: + client_config = BotocoreConfig(user_agent_extra="strands-agents") + + # Override the memory client's boto3 clients + self.memory_client.gmcp_client = session.client( + "bedrock-agentcore-control", region_name=region_name or session.region_name, config=client_config + ) + self.memory_client.gmdp_client = session.client( + "bedrock-agentcore", region_name=region_name or session.region_name, config=client_config + ) + super().__init__(session_id=self.config.session_id, session_repository=self) + def _get_monotonic_timestamp(self) -> datetime: """Generate a monotonically increasing timestamp with microsecond precision.""" with self._timestamp_lock: @@ -95,27 +116,6 @@ def _create_event_with_monotonic_timestamp(self, **kwargs): kwargs['eventTimestamp'] = self._get_monotonic_timestamp() return self.memory_client.gmdp_client.create_event(**kwargs) - # Override the clients if custom boto session or config is provided - # Add strands-agents to the request user agent - if boto_client_config: - existing_user_agent = getattr(boto_client_config, "user_agent_extra", None) - if existing_user_agent: - new_user_agent = f"{existing_user_agent} strands-agents" - else: - new_user_agent = "strands-agents" - client_config = boto_client_config.merge(BotocoreConfig(user_agent_extra=new_user_agent)) - else: - client_config = BotocoreConfig(user_agent_extra="strands-agents") - - # Override the memory client's boto3 clients - self.memory_client.gmcp_client = session.client( - "bedrock-agentcore-control", region_name=region_name or session.region_name, config=client_config - ) - self.memory_client.gmdp_client = session.client( - "bedrock-agentcore", region_name=region_name or session.region_name, config=client_config - ) - super().__init__(session_id=self.config.session_id, session_repository=self) - def _get_full_session_id(self, session_id: str) -> str: """Get the full session ID with the configured prefix. From 41f2a48c1dadc6064bc589b6b9c04af867b62ccf Mon Sep 17 00:00:00 2001 From: Jakob Berg Date: Tue, 16 Sep 2025 20:46:01 -0400 Subject: [PATCH 5/6] fix: dont use milliseconds or microseconds because boto3 doesnt support that currently --- .../integrations/strands/session_manager.py | 10 ++--- .../test_agentcore_memory_session_manager.py | 41 +++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 7656ee57..820966f8 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -3,7 +3,7 @@ import json import logging import threading -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from typing import TYPE_CHECKING, Any, Optional import boto3 @@ -95,7 +95,7 @@ def __init__( super().__init__(session_id=self.config.session_id, session_repository=self) def _get_monotonic_timestamp(self) -> datetime: - """Generate a monotonically increasing timestamp with microsecond precision.""" + """Generate a monotonically increasing timestamp with second precision.""" with self._timestamp_lock: current = datetime.now(timezone.utc) @@ -103,11 +103,9 @@ def _get_monotonic_timestamp(self) -> datetime: self._last_timestamp = current self._sequence_counter = 0 else: - # Same or earlier time - increment sequence and add microseconds + # Same or earlier time - increment sequence and add seconds self._sequence_counter += 1 - self._last_timestamp = self._last_timestamp.replace( - microsecond=min(999999, self._last_timestamp.microsecond + self._sequence_counter) - ) + self._last_timestamp = self._last_timestamp + timedelta(seconds=self._sequence_counter) return self._last_timestamp diff --git a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py index 09d984cf..888d6173 100644 --- a/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py +++ b/tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py @@ -1,6 +1,7 @@ """Tests for AgentCoreMemorySessionManager.""" import threading +from datetime import datetime, timezone from unittest.mock import Mock, patch import pytest @@ -1096,19 +1097,35 @@ def get_timestamp(): for i in range(1, len(timestamps)): assert timestamps[i] > timestamps[i-1] - def test_microsecond_precision(self, session_manager): - """Test that rapid calls get microsecond-level differentiation.""" - # Get many timestamps rapidly - timestamps = [] - for _ in range(100): - timestamps.append(session_manager._get_monotonic_timestamp()) + def test_monotonic_timestamp_past_minute(self, session_manager): + """Test that monotonic timestamps can increment past 1 minute.""" + import unittest.mock + + # Mock datetime.now to return the same time repeatedly + fixed_time = datetime.now(timezone.utc) + + with unittest.mock.patch('bedrock_agentcore.memory.integrations.strands.session_manager.datetime') as mock_datetime: + mock_datetime.now.return_value = fixed_time + mock_datetime.timezone = timezone - # Should have microsecond differences - for i in range(1, len(timestamps)): - diff = timestamps[i] - timestamps[i-1] - assert diff.total_seconds() > 0 - # Should be small differences (microseconds) - assert diff.total_seconds() < 0.001 + timestamps = [] + for i in range(70): + timestamp = session_manager._get_monotonic_timestamp() + timestamps.append(timestamp) + + # Verify timestamps are monotonically increasing + for i in range(1, len(timestamps)): + assert timestamps[i] > timestamps[i-1] + + # Verify we can go past 60 seconds + time_diff = timestamps[-1] - timestamps[0] + assert time_diff.total_seconds() >= 60 + + # Verify we can go past 60 seconds + time_diff = timestamps[-1] - timestamps[0] + assert time_diff.total_seconds() >= 60 + + def test_create_event_wrapper_uses_monotonic_timestamp(self, session_manager): """Test that the create_event wrapper uses monotonic timestamps.""" From bd6cf8f8b4b450ee3d58c744dc33772db64eb572 Mon Sep 17 00:00:00 2001 From: Jakob Berg Date: Tue, 16 Sep 2025 21:00:45 -0400 Subject: [PATCH 6/6] chore: add debug logs --- .../memory/integrations/strands/session_manager.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py index 820966f8..ddf761dc 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/session_manager.py +++ b/src/bedrock_agentcore/memory/integrations/strands/session_manager.py @@ -97,7 +97,11 @@ def __init__( def _get_monotonic_timestamp(self) -> datetime: """Generate a monotonically increasing timestamp with second precision.""" with self._timestamp_lock: - current = datetime.now(timezone.utc) + # Currently, boto3 cuts off any granularity beyond seconds in the CreateEvent request. While we wait for a fix to the + # boto3 client, the best we can do to allow concurrent events is to increment seconds on the client. + # TODO: Once boto3 supports sending milliseconds, we should make this code increment milliseconds instead of seconds. + current = datetime.now(timezone.utc).replace(microsecond=0) + print("LOOK AT CURRENT:", current) if self._last_timestamp is None or current > self._last_timestamp: self._last_timestamp = current @@ -106,8 +110,10 @@ def _get_monotonic_timestamp(self) -> datetime: # Same or earlier time - increment sequence and add seconds self._sequence_counter += 1 self._last_timestamp = self._last_timestamp + timedelta(seconds=self._sequence_counter) + + print("LOOK AT lastTimestamp:", self._last_timestamp) - return self._last_timestamp + return self._last_timestamp.replace(microsecond=0) def _create_event_with_monotonic_timestamp(self, **kwargs): """Create event with guaranteed monotonic timestamp."""