-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: reload session file on read to prevent stale chat history #1764
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -405,14 +405,28 @@ def test_invalidate_cache(self, temp_store): | |
| with open(filepath, "w") as f: | ||
| json.dump(data, f) | ||
|
|
||
| # Without invalidation, cache returns old data | ||
| # Reads reload from disk when the session file exists | ||
| history = temp_store.get_chat_history("session-1") | ||
| assert len(history) == 1 # Cached | ||
| # After invalidation, new data is loaded | ||
| assert len(history) == 2 | ||
|
|
||
| # invalidate_cache still clears in-memory state for non-existent files | ||
| temp_store.invalidate_cache("session-1") | ||
| history = temp_store.get_chat_history("session-1") | ||
| assert len(history) == 2 | ||
|
|
||
| def test_get_chat_history_sees_other_store_instance(self, temp_store): | ||
| """Another store on the same session_dir must see new messages without invalidate_cache.""" | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| writer = DefaultSessionStore(session_dir=tmpdir) | ||
| reader = DefaultSessionStore(session_dir=tmpdir) | ||
|
|
||
| writer.add_user_message("session-1", "first") | ||
| reader._load_session("session-1") | ||
| writer.add_user_message("session-1", "second") | ||
|
|
||
| history = reader.get_chat_history("session-1") | ||
| assert len(history) == 2 | ||
| assert history[1]["content"] == "second" | ||
|
Comment on lines
+417
to
+429
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add agent-level regression coverage for this feature path. This new regression test is useful, but it only validates store internals. Per repo policy, this feature also needs a real agentic test ( As per coding guidelines, "Real agentic tests are MANDATORY for every feature: Agent must call agent.start() with a real prompt, call the LLM, and produce actual text response—not just smoke tests of object construction". 🤖 Prompt for AI Agents |
||
|
|
||
| def test_list_sessions_with_none_updated_at(self, temp_store): | ||
| """Test list_sessions handles None updated_at values without crashing. | ||
|
|
||
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.
Avoid TOCTOU race between existence check and cache return.
Line 271 checks file existence before locking, but Lines 279-280 can still return cached data if another instance creates/writes the file in between. That can reintroduce a stale-read edge case on this path.
Suggested fix
def _load_session(self, session_id: str) -> SessionData: """Load session from disk with file locking.""" filepath = self._get_session_path(session_id) - - # When a session file exists, always reload under FileLock so reads - # from another DefaultSessionStore instance (or process) are visible. - if os.path.exists(filepath): - with FileLock(filepath, self.lock_timeout): - session = self._load_session_from_disk(session_id, filepath) - with self._lock: - self._cache[session_id] = session - return session - - with self._lock: - if session_id in self._cache: - return self._cache[session_id] - session = SessionData(session_id=session_id) - self._cache[session_id] = session - return session + # Serialize existence/read decision to avoid stale cache races. + with FileLock(filepath, self.lock_timeout): + if os.path.exists(filepath): + session = self._load_session_from_disk(session_id, filepath) + else: + with self._lock: + session = self._cache.get(session_id) or SessionData(session_id=session_id) + + with self._lock: + self._cache[session_id] = session + return session🤖 Prompt for AI Agents