-
Notifications
You must be signed in to change notification settings - Fork 24
What time is it ! #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
What time is it ! #102
Conversation
Attempt to tackle redis#71 - various approach possible. here is one
|
the clock skew is configurable : MAX_FUTURE_TIMESTAMP_SECONDS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements timestamp validation for messages in working memory to address incorrect message ordering and lost temporal context. It introduces a phased, backward-compatible approach where clients are encouraged to provide created_at timestamps, with deprecation warnings for those who don't, and validation to prevent future timestamps beyond clock skew tolerance.
Key Changes
- Added optional timestamp validation with
REQUIRE_MESSAGE_TIMESTAMPSconfig flag (default:false) - Implemented deprecation warning system with rate-limited logging when timestamps are missing
- Added
X-Deprecation-Warningresponse header to alert API clients - Future timestamp validation with 5-minute clock skew tolerance
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
agent_memory_server/models.py |
Added model_validator to MemoryMessage for timestamp validation, rate-limited warnings via class-level set, and _created_at_was_provided tracking |
agent_memory_server/config.py |
Added require_message_timestamps and max_future_timestamp_seconds settings |
agent_memory_server/api.py |
Refactored put_working_memory into core function and endpoint wrapper; added deprecation header logic |
agent_memory_server/mcp.py |
Updated to use put_working_memory_core instead of put_working_memory |
agent-memory-client/agent_memory_client/models.py |
Mirrored server validation logic without _created_at_was_provided tracking |
tests/test_models.py |
Added 10 tests for timestamp validation covering warnings, rate limiting, and future timestamp rejection |
tests/test_api.py |
Added 4 tests for deprecation header behavior in various scenarios |
docs/working-memory.md |
Added timestamp guidance, example usage with timestamps, and deprecation notice |
docs/configuration.md |
Documented timestamp validation settings and behavior |
docs/api.md |
Updated PUT endpoint example to include created_at timestamps |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def __init__(self, **data): | ||
| # Check if created_at was provided before calling super().__init__ | ||
| created_at_provided = "created_at" in data and data["created_at"] is not None | ||
| super().__init__(**data) | ||
| self._created_at_was_provided = created_at_provided |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The __init__ method checks for created_at in the data dictionary and stores the result in _created_at_was_provided. However, this logic duplicates the same check performed in the validate_created_at model_validator. Additionally, the __init__ is called after the validator runs (validators run during super().__init__()), so this creates a potential inconsistency.
Consider removing the custom __init__ and instead setting _created_at_was_provided within the validator itself by modifying the data dict to include a marker, or use a different approach like checking if the value equals the default at the API layer.
|
|
||
| if created_at_value > max_allowed: | ||
| raise ValueError( | ||
| f"created_at cannot be in the future. " |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message states "created_at cannot be in the future" but should be more specific: "created_at cannot be more than {tolerance} seconds in the future" to accurately reflect the validation logic. The current message is misleading since timestamps slightly in the future (within tolerance) are actually allowed.
| f"created_at cannot be in the future. " | |
| f"created_at cannot be more than {settings.max_future_timestamp_seconds} seconds in the future. " |
| f"created_at cannot be in the future. " | ||
| f"Received: {created_at_value.isoformat()}, " | ||
| f"Max allowed (with {cls._max_future_seconds}s tolerance): " | ||
| f"{max_allowed.isoformat()}" |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message states "created_at cannot be in the future" but should be more specific: "created_at cannot be more than {tolerance} seconds in the future" to accurately reflect the validation logic. The current message is misleading since timestamps slightly in the future (within tolerance) are actually allowed.
| f"created_at cannot be in the future. " | |
| f"Received: {created_at_value.isoformat()}, " | |
| f"Max allowed (with {cls._max_future_seconds}s tolerance): " | |
| f"{max_allowed.isoformat()}" | |
| f"created_at cannot be more than {cls._max_future_seconds} seconds in the future. " | |
| f"Received: {created_at_value.isoformat()}, " | |
| f"Max allowed: {max_allowed.isoformat()}" |
| created_at=datetime.now(UTC) # Provide timestamp | ||
| ), | ||
| MemoryMessage( | ||
| role="assistant", | ||
| content="That sounds exciting! What type of activities are you interested in?", | ||
| id=ulid.ULID(), | ||
| created_at=datetime.now(UTC) | ||
| ), | ||
| MemoryMessage( | ||
| role="user", | ||
| content="I love museums and good food", | ||
| id=ulid.ULID(), | ||
| created_at=datetime.now(UTC) | ||
| ) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation example uses datetime.now(UTC) for all three messages, which would give them identical timestamps. This defeats the purpose of providing timestamps for accurate message ordering. Consider using slightly different timestamps or adding a comment explaining that in real usage, timestamps should reflect actual message creation times.
| if msg_id not in cls._warned_message_ids: | ||
| # Prevent unbounded memory growth | ||
| if len(cls._warned_message_ids) >= cls._max_warned_ids: | ||
| cls._warned_message_ids.clear() | ||
| cls._warned_message_ids.add(msg_id) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class-level set _warned_message_ids is not thread-safe. In a multi-threaded or async environment (which FastAPI uses), concurrent access to check membership, check length, clear, and add operations can lead to race conditions. This could result in:
- Duplicate warnings being logged
- The set growing beyond _max_warned_ids
- Messages being lost during clear operations
Consider using a thread-safe alternative like threading.Lock to protect these operations, or use a more robust rate-limiting mechanism like TTL-based caching (e.g., with functools.lru_cache or a dedicated rate-limiting library).
| if msg_id not in cls._warned_message_ids: | ||
| # Prevent unbounded memory growth | ||
| if len(cls._warned_message_ids) >= cls._max_warned_ids: | ||
| cls._warned_message_ids.clear() | ||
| cls._warned_message_ids.add(msg_id) |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same thread-safety issue exists in the client model. The class-level set _warned_message_ids is not thread-safe, which could cause race conditions when multiple threads create MemoryMessage instances concurrently. Consider adding thread-safe protection or using a more robust rate-limiting mechanism.
| | Setting | Behavior | | ||
| |---------|----------| | ||
| | `REQUIRE_MESSAGE_TIMESTAMPS=false` (default) | Auto-generates missing timestamps with deprecation warning | | ||
| | `REQUIRE_MESSAGE_TIMESTAMPS=true` | Rejects messages without `created_at` with 400 error | |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states that messages without created_at will be rejected with a "400 error" when REQUIRE_MESSAGE_TIMESTAMPS=true. However, Pydantic validation errors in FastAPI typically return 422 Unprocessable Entity status codes, not 400. Update this to "422 error" for accuracy, or verify that custom error handling returns 400.
| | `REQUIRE_MESSAGE_TIMESTAMPS=true` | Rejects messages without `created_at` with 400 error | | |
| | `REQUIRE_MESSAGE_TIMESTAMPS=true` | Rejects messages without `created_at` with 422 error | |
Attempt to tackle #71 - various approach possible. here is one
Summary
Implements timestamp validation for messages in working memory with backward compatibility. Clients are now expected to provide
created_attimestamps for messages, with deprecation warnings for those who don't.Closes #71
Problem
Currently, clients send messages to working memory without associated dates. The server auto-generates timestamps at deserialization time, which causes:
Solution
A phased approach with full backward compatibility:
Phase 1 (This PR) - Non-Breaking
created_atis not provided (rate-limited)X-Deprecation-Warningresponse header to alert clientscreated_atis not in the future (with 5-minute clock skew tolerance)REQUIRE_MESSAGE_TIMESTAMPSconfig flag (default:false)Phase 2 (Next Major Version)
REQUIRE_MESSAGE_TIMESTAMPSdefault totrueChanges
Configuration (
agent_memory_server/config.py)require_message_timestamps: bool = False- opt-in enforcementmax_future_timestamp_seconds: int = 300- clock skew tolerance (5 minutes)Model Validation (
agent_memory_server/models.py)model_validator(mode="before")toMemoryMessageclass_created_at_was_providedprivate attribute for header logicAPI Changes (
agent_memory_server/api.py)X-Deprecation-Warningheader to PUT/v1/working-memory/{session_id}when messages lack timestampsput_working_memory_core()for reuse by MCPMCP Changes (
agent_memory_server/mcp.py)put_working_memory_coreinstead ofput_working_memoryClient Changes (
agent-memory-client/agent_memory_client/models.py)Tests
tests/test_models.py: AddedTestMemoryMessageTimestampValidation(10 tests)tests/test_api.py: AddedTestDeprecationHeader(4 tests)Documentation
docs/working-memory.md: Added timestamp guidance and deprecation noticedocs/configuration.md: AddedREQUIRE_MESSAGE_TIMESTAMPSandMAX_FUTURE_TIMESTAMP_SECONDSsettingsdocs/api.md: Updated PUT endpoint example withcreated_atand deprecation header infoAPI Behavior
Default Mode (
REQUIRE_MESSAGE_TIMESTAMPS=false)Strict Mode (
REQUIRE_MESSAGE_TIMESTAMPS=true)Future Timestamp Rejection
Migration Guide for Clients
Before (deprecated)
After (recommended)
Testing
Test Results
Checklist