Skip to content

fix: reload session from disk on get_chat_history reads#1759

Merged
MervinPraison merged 2 commits into
mainfrom
cursor/critical-bug-investigation-b96f
May 30, 2026
Merged

fix: reload session from disk on get_chat_history reads#1759
MervinPraison merged 2 commits into
mainfrom
cursor/critical-bug-investigation-b96f

Conversation

@cursor
Copy link
Copy Markdown
Contributor

@cursor cursor Bot commented May 27, 2026

Bug and impact

DefaultSessionStore.get_chat_history() (and get_session()) could return a stale in-memory cache after another store instance wrote new messages to the same session file on disk.

Concrete trigger: Two DefaultSessionStore instances share a session_dir (common in tests, gateway/bot splits, or multi-worker deployments). Instance A calls _load_session, then instance B appends a message; instance A's get_chat_history still returns only the cached messages.

User impact: BotSessionManager._load_history() feeds the LLM truncated chat context — missing recent turns. Same blast radius as the session write races fixed in #1724/#1727, but on the read path.

Root cause

Write paths (add_message, _modify_session_locked) reload from disk under FileLock. Read paths used _load_session(), which returns the cache without checking disk when session_id is already cached.

Fix

  • Add _read_session_fresh() to reload under FileLock and refresh the cache.
  • Use it from get_chat_history and get_session.
  • Regression test: test_get_chat_history_sees_writes_from_other_store.

Validation

  • Reproduction script confirmed stale read before fix, correct 2-message history after.
  • pytest src/praisonai-agents/tests/unit/session/test_session_store.py::TestDefaultSessionStore::test_get_chat_history_sees_writes_from_other_store passes.

Investigation context

Daily critical-bug scan (2026-05-27). Recent commits (#1724#1737) fixed session write races; this addresses the complementary read staleness. No other new critical issues found in that window.

Open in Web View Automation 

Summary by CodeRabbit

  • Bug Fixes

    • Session data now properly reloads the latest changes from disk, ensuring concurrent access to shared sessions stays synchronized.
  • Tests

    • Added test to verify session data consistency across multiple instances.

Review Change Stack

get_chat_history and get_session returned stale in-memory cache when
another DefaultSessionStore instance (or worker process with a warm
cache) had written newer messages to the same session file. Bot
load_history paths then fed the LLM truncated context.

Reload under FileLock before returning reads; add regression test.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison
Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison
Copy link
Copy Markdown
Owner

/review

@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 189368d0-5b9e-4e2c-8c90-dc652b96b798

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a fresh-read mechanism to prevent session cache staleness. A new _read_session_fresh() helper reloads session JSON from disk under FileLock and refreshes the in-memory cache. The get_chat_history() and get_session() methods are updated to use this path, ensuring they reflect the latest persisted state rather than returning potentially stale cached data. A test validates cross-instance cache invalidation by having separate reader and writer store instances access the same session file.

Changes

Session Cache Staleness Fix

Layer / File(s) Summary
Fresh session read implementation and usage
src/praisonai-agents/praisonaiagents/session/store.py, src/praisonai-agents/tests/unit/session/test_session_store.py
_read_session_fresh() helper acquires FileLock, reloads session JSON from disk, and refreshes in-memory cache. get_chat_history() and get_session() call this helper to return current persisted state. Test verifies reader observes writes from separate writer instance to same session file.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

  • PraisonAIDocs#417: Both changes address DefaultSessionStore cache staleness by adding reload-under-lock helpers and updating read paths to prevent stale data.

Poem

🐰 A session refreshed from disk so true,
No stale cache shall trick the view,
Lock and reload, the data flows,
Cross-instance harmony now grows!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly describes the main change: fixing stale cache reads in DefaultSessionStore by reloading session data from disk on get_chat_history calls.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/critical-bug-investigation-b96f

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MervinPraison
Copy link
Copy Markdown
Owner

@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.

Review areas:

  1. Bloat check: Are changes minimal and focused? Any unnecessary code or scope creep?
  2. Security: Any hardcoded secrets, unsafe eval/exec, missing input validation?
  3. Performance: Any module-level heavy imports? Hot-path regressions?
  4. Tests: Are tests included? Do they cover the changes adequately?
  5. Backward compat: Any public API changes without deprecation?
  6. Code quality: DRY violations, naming conventions, error handling?
  7. Address reviewer feedback: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review
  8. Suggest specific improvements with code examples where possible

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/praisonai-agents/praisonaiagents/session/store.py (1)

515-515: ⚡ Quick win

Consider consistent fresh-read behavior for bulk agent queries.

While get_session() now reloads from disk to prevent stale reads, get_sessions_by_agent() (line 658) still calls _load_session(), which uses cached data. This creates inconsistent staleness guarantees across read methods.

For strict multi-process correctness, get_sessions_by_agent() should also use _read_session_fresh():

return [self._read_session_fresh(sid) for sid in session_ids]

However, this may be intentionally deferred for performance reasons in bulk operations. Consider documenting the caching behavior difference or unifying the approach in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/session/store.py` at line 515,
get_sessions_by_agent() currently calls the cached loader (_load_session())
while get_session() uses _read_session_fresh(), causing inconsistent staleness
guarantees; change get_sessions_by_agent() (where it iterates session_ids) to
call _read_session_fresh(session_id) for each id to ensure fresh disk reads, or
alternatively add a clear docstring note in get_sessions_by_agent() explaining
that it intentionally uses cached _load_session() for performance and may return
stale data—update the function implementation or its documentation accordingly
and reference get_session(), get_sessions_by_agent(), _load_session(),
_read_session_fresh(), and session_ids in your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/praisonai-agents/praisonaiagents/session/store.py`:
- Line 509: The test test_invalidate_cache relies on observable caching via
get_chat_history(), but the session loader now always reloads from disk using
_read_session_fresh(), so update or remove the test: either delete
test_invalidate_cache if caching is now internal-only, or modify it to assert
that get_chat_history() reflects the externally edited file immediately (expect
2 messages) and adjust any assertions around invalidate_cache() accordingly;
locate assertions in test_invalidate_cache and update expected message counts to
match the always-reload behavior of get_chat_history() and
_read_session_fresh().

---

Nitpick comments:
In `@src/praisonai-agents/praisonaiagents/session/store.py`:
- Line 515: get_sessions_by_agent() currently calls the cached loader
(_load_session()) while get_session() uses _read_session_fresh(), causing
inconsistent staleness guarantees; change get_sessions_by_agent() (where it
iterates session_ids) to call _read_session_fresh(session_id) for each id to
ensure fresh disk reads, or alternatively add a clear docstring note in
get_sessions_by_agent() explaining that it intentionally uses cached
_load_session() for performance and may return stale data—update the function
implementation or its documentation accordingly and reference get_session(),
get_sessions_by_agent(), _load_session(), _read_session_fresh(), and session_ids
in your change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79bf613f-3113-484a-a897-cca77f8ed4f7

📥 Commits

Reviewing files that changed from the base of the PR and between 8acf77c and 0889173.

📒 Files selected for processing (2)
  • src/praisonai-agents/praisonaiagents/session/store.py
  • src/praisonai-agents/tests/unit/session/test_session_store.py

List of {"role": "user/assistant", "content": "..."} dicts.
"""
session = self._load_session(session_id)
session = self._read_session_fresh(session_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Existing test test_invalidate_cache will fail with this change.

The test at lines 410-429 expects get_chat_history() to return cached data (1 message) before invalidate_cache() is called, but the new implementation always reloads from disk via _read_session_fresh(), so it will see the directly-edited file (2 messages) and the assertion at line 424 will fail.

The test needs to be updated to reflect the new always-reload behavior, or the test should be removed if caching is no longer observable through the public API.

Proposed test fix

The test should either be removed (since cache is now an internal optimization detail not observable via public API), or updated to verify that reads always see the latest disk state:

 def test_invalidate_cache(self, temp_store):
-    """Test cache invalidation."""
+    """Test that reads always see latest disk state."""
     temp_store.add_user_message("session-1", "Hello")
     
     # Modify file directly
     filepath = os.path.join(temp_store.session_dir, "session-1.json")
     with open(filepath, "r") as f:
         data = json.load(f)
     data["messages"].append({"role": "user", "content": "Direct edit", "timestamp": time.time(), "metadata": {}})
     with open(filepath, "w") as f:
         json.dump(data, f)
     
-    # Without invalidation, cache returns old data
+    # Reads always reload from disk (no stale cache)
     history = temp_store.get_chat_history("session-1")
-    assert len(history) == 1  # Cached
+    assert len(history) == 2  # Fresh from disk
     
-    # After invalidation, new data is loaded
-    temp_store.invalidate_cache("session-1")
-    history = temp_store.get_chat_history("session-1")
-    assert len(history) == 2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/session/store.py` at line 509, The test
test_invalidate_cache relies on observable caching via get_chat_history(), but
the session loader now always reloads from disk using _read_session_fresh(), so
update or remove the test: either delete test_invalidate_cache if caching is now
internal-only, or modify it to assert that get_chat_history() reflects the
externally edited file immediately (expect 2 messages) and adjust any assertions
around invalidate_cache() accordingly; locate assertions in
test_invalidate_cache and update expected message counts to match the
always-reload behavior of get_chat_history() and _read_session_fresh().

@MervinPraison
Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions

Phase 2: FIX Valid Issues
4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
5. Push all code fixes directly to THIS branch (do NOT create a new PR)
6. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
7. If all issues are resolved, approve the PR / close the Issue
8. If blocking issues remain, request changes / leave clear action items

@praisonai-triage-agent
Copy link
Copy Markdown
Contributor

praisonai-triage-agent Bot commented May 27, 2026

Claude finished @MervinPraison's task —— View job


Final Architecture Review ✅

Phase 1: Review per AGENTS.md

  • Read AGENTS.md architecture guidelines
  • Validate protocol-driven design compliance ✅ Clean fix, no heavy implementations added
  • Check backward compatibility ✅ Zero breaking changes
  • Assess performance impact ✅ Minor—only affects read paths when necessary

Phase 2: Address Valid Issues

  • Examine CodeRabbit's test failure issue ✅ CONFIRMED VALID - test fails as expected
  • Review staleness consistency concern ✅ VALID - get_sessions_by_agent() inconsistency exists
  • Implement fixes for valid issues ✅ ALL FIXED
  • Push fixes to branch ✅ PUSHED (commit 4e3031bb)

Phase 3: Final Verdict

  • Run validation tests ✅ ALL PASSING
  • Provide approval ✅ APPROVED

Summary of Changes Made ✅

Original PR: Excellent fix for session cache staleness that correctly addresses the root cause by reloading from disk under FileLock.

Issues Fixed:

  1. Test Failure: Updated test_invalidate_cache() to reflect new always-reload behavior
  2. Consistency: Fixed get_sessions_by_agent() to use _read_session_fresh() for consistent staleness guarantees

Final Architectural Assessment ✅

✅ APPROVED - This is a high-quality fix that:

  • Follows protocol-driven design principles
  • Maintains backward compatibility
  • Addresses a critical session staleness bug
  • Has minimal performance impact
  • Includes proper regression tests

The additional fixes ensure test consistency and eliminate the staleness inconsistency between different read methods. All tests pass and the implementation is architecturally sound.


Modified Files:

  • praisonaiagents/session/store.py - Fixed get_sessions_by_agent() consistency
  • tests/unit/session/test_session_store.py - Updated test_invalidate_cache() for new behavior

Branch: cursor/critical-bug-investigation-b96f

- Update test_invalidate_cache to reflect new always-reload behavior
- Fix inconsistency in get_sessions_by_agent() to use fresh reads
- Ensures consistent staleness guarantees across all read methods

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison marked this pull request as ready for review May 30, 2026 06:20
Copilot AI review requested due to automatic review settings May 30, 2026 06:20
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@MervinPraison MervinPraison merged commit c1db631 into main May 30, 2026
6 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 30, 2026

Greptile Summary

This PR fixes read-path staleness in DefaultSessionStore where get_chat_history and get_session could return a cached snapshot after another store instance (separate process, worker, or test fixture) had written new messages to the same session file. The fix introduces _read_session_fresh() which always acquires a FileLock and reads from disk, bypassing the in-memory cache, and wires it into all public read paths.

  • _read_session_fresh() replaces the cache-first _load_session() call in get_chat_history, get_session, and get_sessions_by_agent, guaranteeing disk-fresh reads at the cost of a lock + I/O on every call.
  • A targeted regression test confirms that a second store instance sees messages written by the first, and test_invalidate_cache is updated to reflect that reads now always bypass the stale cache.

Confidence Score: 4/5

Safe to merge; the fix correctly eliminates stale-read staleness on the read path and the new test validates the scenario end-to-end.

The core logic is sound — replacing the cache-first path with a disk-always read under FileLock is the right approach for the described multi-instance problem. Two non-blocking concerns remain: a narrow window between the FileLock release and the in-process lock acquisition where a concurrent write could leave the cache in a slightly older state (harmless in practice since no production code calls _load_session anymore), and get_sessions_by_agent now reads each session file twice due to the unprotected directory scan in list_sessions_by_agent followed by the locked re-read in _read_session_fresh.

store.py — specifically _read_session_fresh and get_sessions_by_agent

Important Files Changed

Filename Overview
src/praisonai-agents/praisonaiagents/session/store.py Adds _read_session_fresh() to unconditionally reload from disk under FileLock and replaces the cached _load_session() path in get_chat_history, get_session, and get_sessions_by_agent. Two P2 concerns: a narrow race where a fresher cache entry can be overwritten after the FileLock is released, and a double file scan in get_sessions_by_agent.
src/praisonai-agents/tests/unit/session/test_session_store.py Adds test_get_chat_history_sees_writes_from_other_store and updates test_invalidate_cache to match the new always-reload-from-disk semantics. Tests are accurate and cover the intended regression scenario.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant StoreA as DefaultSessionStore A
    participant StoreB as DefaultSessionStore B
    participant FileLock
    participant Disk

    Note over StoreA,StoreB: Before fix — stale cache
    StoreB->>Disk: add_message("first") [write V1]
    StoreA->>StoreA: _load_session() → cache miss → reads V1
    StoreB->>Disk: add_message("second") [write V2]
    Caller->>StoreA: get_chat_history()
    StoreA->>StoreA: _load_session() → cache hit → returns V1 (stale!)

    Note over StoreA,StoreB: After fix — always fresh
    StoreB->>Disk: add_message("first") [write V1]
    StoreA->>StoreA: _load_session() → cache primed with V1 (test setup)
    StoreB->>Disk: add_message("second") [write V2]
    Caller->>StoreA: get_chat_history()
    StoreA->>FileLock: acquire(session.json)
    FileLock-->>StoreA: locked
    StoreA->>Disk: _load_session_from_disk() → reads V2
    StoreA->>FileLock: release
    StoreA->>StoreA: update cache with V2
    StoreA-->>Caller: returns V2 (fresh ✓)
Loading

Reviews (1): Last reviewed commit: "fix: address session store test failure ..." | Re-trigger Greptile

Comment on lines +306 to +313
def _read_session_fresh(self, session_id: str) -> SessionData:
"""Reload session from disk and refresh the in-process cache."""
filepath = self._get_session_path(session_id)
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Stale cache overwrite in narrow race window

Between releasing FileLock and acquiring self._lock, another thread or process can call _modify_session_locked, which writes a newer version to disk and updates the cache. When _read_session_fresh then acquires self._lock, it overwrites that newer cached value with the slightly older snapshot it read under the lock. Because all public read paths now always call _read_session_fresh, the next caller will re-read from disk and recover — but any code that still calls _load_session (including the tests that prime the cache) will observe the downgraded value until the next disk read.

Comment on lines 657 to +658
session_ids = self.list_sessions_by_agent(agent_name, limit)
return [self._load_session(sid) for sid in session_ids]
return [self._read_session_fresh(sid) for sid in session_ids]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Double file scan per session in get_sessions_by_agent

list_sessions_by_agent (called on line 657) already reads every JSON file in the directory once (without FileLock) to filter by agent_name. Then _read_session_fresh re-reads each matched session file a second time under FileLock. For an agent with many sessions this results in 2 × N full file reads in a tight loop, holding a separate lock for each. The list scan reads are also unprotected, so a write that happens between the two reads will cause list_sessions_by_agent and _read_session_fresh to disagree on the session content — the latter will always win, but the redundant scan adds latency for no correctness benefit.

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