Skip to content

fix: prevent duplicate session messages from persist+auto_save and save_state#1897

Draft
cursor[bot] wants to merge 2 commits into
mainfrom
cursor/critical-bug-investigation-76ce
Draft

fix: prevent duplicate session messages from persist+auto_save and save_state#1897
cursor[bot] wants to merge 2 commits into
mainfrom
cursor/critical-bug-investigation-76ce

Conversation

@cursor

@cursor cursor Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Bug and impact

Two related session persistence bugs caused duplicate chat messages to be written to disk:

  1. memory="history" + auto_save: Per-turn _persist_message() saved each message, then _auto_save_session() re-appended the full history because _auto_save_last_index was never updated by the per-turn path. A single agent turn produced 4 messages instead of 2.

  2. Session.save_state(): _save_agent_chat_histories() called add_message() for every message on each save, so repeated save_state() calls doubled/tripled history.

Impact: Corrupted session files, bloated context windows, incorrect history on restore, and model confusion from duplicate turns.

Root cause

  • _persist_message and _auto_save_session both write to the same session ID but only _auto_save_session tracked the last-saved index.
  • _save_agent_chat_histories used append semantics (add_message) instead of replace semantics for full-history saves.

Fix

  • Sync _auto_save_last_index in _persist_message when auto_save == _session_id.
  • Use atomic set_chat_history() in Session._save_agent_chat_histories for idempotent saves.

Validation

  • Added regression tests in test_memory_history_separation.py
  • All 15 tests in that file pass
  • Reproduced both bugs before fix (4 messages instead of 2); verified fix produces exactly 2
Open in WebΒ View AutomationΒ 

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed duplicate chat history entries when using auto-save with memory separation.
    • Improved session persistence to ensure auto-save bookkeeping stays in sync with stored messages.
    • Enhanced session state saving to prevent unintended history duplication on repeated operations.

…ve_state

When memory=history and auto_save share a session_id, per-turn _persist_message
wrote messages that _auto_save_session then re-appended because
_auto_save_last_index was not updated.

Session.save_state() also re-appended the full chat history on every call
instead of replacing it atomically.

- Sync _auto_save_last_index in _persist_message when sessions match
- Use set_chat_history() in Session._save_agent_chat_histories for idempotent saves
- Add regression tests for both duplicate paths

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

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
βœ… Action performed

Review finished.

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

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 98691057-696b-48db-b1b5-a93c9d8f3c82

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

Session and agent memory persistence are refactored to prevent duplicate message entries: Session._save_agent_chat_histories() now atomically replaces history via set_chat_history when available, and MemoryMixin._persist_message synchronizes the auto-save index. Tests verify no duplicates occur on repeated saves.

Changes

Chat History Duplicate Prevention

Layer / File(s) Summary
Session store atomic history replacement
src/praisonai-agents/praisonaiagents/session/api.py
Session._save_agent_chat_histories() builds a normalized messages list and atomically replaces stored chat history via set_chat_history() when available, preventing duplicate appends on repeated save_state() calls. Falls back to per-message add_message if the setter is unavailable.
Auto-save index synchronization
src/praisonai-agents/praisonaiagents/agent/memory_mixin.py
MemoryMixin._persist_message() now conditionally increments _auto_save_last_index after persisting a message when auto_save matches the current session ID, keeping the saved-index counter aligned with persisted turns.
Duplicate-prevention test coverage
src/praisonai-agents/tests/unit/test_memory_history_separation.py
TestPersistAutoSaveNoDuplicates verifies no duplicate history entries when an agent with memory="history" and auto_save enabled persists turns via _persist_message() and _auto_save_session(). TestSessionSaveStateNoDuplicates verifies idempotency by confirming that calling _save_agent_chat_histories() twice does not grow the stored chat history.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • MervinPraison/PraisonAI#932: Both PRs modify the session lifecycle logic that saves agent chat histories to persist chat history across sessions, with the main PR refactoring persistence to avoid duplicates and the related PR introducing the initial _save_agent_chat_histories() and restore helpers.
  • MervinPraison/PraisonAI#1768: The main PR's switch to atomic set_chat_history() replacement is directly tied to the retrieved PR's introduction and testing of DefaultSessionStore.set_chat_history() for under-lock replacement semantics.

Suggested reviewers

  • MervinPraison

Poem

🐰 A hop through history, messages once did double,
Now atomic saves erase that storage trouble!
Indexes dance in sync, no duplicates remain,
Sessions persist with clarityβ€”tests prove no pain! ✨

πŸš₯ 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 title clearly and specifically identifies the main fix: preventing duplicate session messages from two distinct issues (persist+auto_save and save_state).
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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-76ce

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

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
src/praisonai-agents/praisonaiagents/agent/memory_mixin.py (1)

406-410: πŸ’€ Low value

Potential race condition on _auto_save_last_index update.

Lines 408-410 perform a read-modify-write on _auto_save_last_index without acquiring _history_lock. If multiple threads call _persist_message concurrently, the increment can be lost (both threads read the same value and write the same incremented value), causing _auto_save_session to re-save already-persisted messages on the next call.

Typical agent usage is single-threaded (one turn at a time), so this race is unlikely in practice. However, for consistency with other MemoryMixin methods that protect shared state (e.g., _add_to_chat_history at line 81, _get_chat_history_length at line 109), consider wrapping the index update in with self._history_lock: or document that _persist_message must be called serially.

πŸ€– 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/agent/memory_mixin.py` around lines 406
- 410, The read-modify-write of _auto_save_last_index in _persist_message can
race; wrap the increment and getattr of self._auto_save_last_index inside the
existing _history_lock to make the update atomic (e.g., inside with
self._history_lock: read current value via getattr, increment, and assign), and
keep the surrounding conditional that checks self.auto_save and
self._session_id; alternatively, if you prefer not to change concurrency
behavior, add a comment on _persist_message documenting that calls must be
serialized and referencing _auto_save_last_index and _auto_save_session.
src/praisonai-agents/praisonaiagents/session/api.py (1)

353-361: πŸ’€ Low value

Fallback path preserves duplicate-append behavior; empty messages silently skipped.

Two edge cases to consider:

  1. Lines 355-361: When set_chat_history is unavailable, the fallback iterates and calls add_message for every message on each save_state() invocation. This preserves the original duplicate-append bug for backends without atomic replacement. If this is intentional for backward compatibility, consider documenting it with a comment or logging a warning on first use.

  2. Line 353: When messages is empty (e.g., chat_history contains only non-dict entries), both the atomic and fallback paths are skipped silently. If this is expected, no action needed; otherwise, consider logging a debug message to help diagnose missing persistence.

πŸ€– 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/api.py` around lines 353 - 361,
The fallback path currently appends each message via session_store.add_message
and so re-introduces duplicate-append bugs for backends that lack
session_store.set_chat_history; additionally an empty messages list is silently
ignored. Change save_state() so that when messages is falsy you emit a debug log
(or no-op with a comment) to make missing persistence visible, and when
session_store lacks set_chat_history attempt an atomic replace: if the store
exposes a clear/delete method (e.g. session_store.clear_chat_history or
session_store.delete_chat_history) call that first for session_id, otherwise
read existing history and replace/deduplicate before looping, and if neither is
possible emit a one-time warning indicating the backend will append (so callers
know to migrate) β€” reference session_store.set_chat_history,
session_store.add_message, session_id, messages, and save_state().
πŸ€– 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.

Nitpick comments:
In `@src/praisonai-agents/praisonaiagents/agent/memory_mixin.py`:
- Around line 406-410: The read-modify-write of _auto_save_last_index in
_persist_message can race; wrap the increment and getattr of
self._auto_save_last_index inside the existing _history_lock to make the update
atomic (e.g., inside with self._history_lock: read current value via getattr,
increment, and assign), and keep the surrounding conditional that checks
self.auto_save and self._session_id; alternatively, if you prefer not to change
concurrency behavior, add a comment on _persist_message documenting that calls
must be serialized and referencing _auto_save_last_index and _auto_save_session.

In `@src/praisonai-agents/praisonaiagents/session/api.py`:
- Around line 353-361: The fallback path currently appends each message via
session_store.add_message and so re-introduces duplicate-append bugs for
backends that lack session_store.set_chat_history; additionally an empty
messages list is silently ignored. Change save_state() so that when messages is
falsy you emit a debug log (or no-op with a comment) to make missing persistence
visible, and when session_store lacks set_chat_history attempt an atomic
replace: if the store exposes a clear/delete method (e.g.
session_store.clear_chat_history or session_store.delete_chat_history) call that
first for session_id, otherwise read existing history and replace/deduplicate
before looping, and if neither is possible emit a one-time warning indicating
the backend will append (so callers know to migrate) β€” reference
session_store.set_chat_history, session_store.add_message, session_id, messages,
and save_state().

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6c44b4e-bf46-4a81-9c3d-5661cb8ca887

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between ce97667 and 4fa806d.

πŸ“’ Files selected for processing (3)
  • src/praisonai-agents/praisonaiagents/agent/memory_mixin.py
  • src/praisonai-agents/praisonaiagents/session/api.py
  • src/praisonai-agents/tests/unit/test_memory_history_separation.py

@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

praisonai-triage-agent Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MervinPraison's task β€”β€” View job


Final Architecture Review βœ…

Phase 1: Review per AGENTS.md

  • Read AGENTS.md architecture guidelines
  • Analyze protocol-driven design compliance
  • Check backward compatibility
  • Verify performance considerations

Phase 2: Fix Valid Issues

  • Analyze CodeRabbit's race condition finding
  • Analyze CodeRabbit's fallback behavior finding
  • Implement fixes for valid issues
  • Push fixes to PR branch

Phase 3: Final Verdict

  • Approve PR with fixes applied

Architecture Assessment βœ…

Protocol-driven design: βœ… EXCELLENT

  • Changes properly use existing SessionStoreProtocol interface
  • No heavy implementations in core SDK
  • Clean separation between protocol and implementation

Backward compatibility: βœ… MAINTAINED

  • Fallback paths preserve existing behavior
  • No breaking API changes
  • Graceful handling of backends without set_chat_history()

Performance: βœ… IMPROVED

  • Atomic set_chat_history() replaces multiple add_message() calls
  • No hot-path regressions
  • Eliminates duplicate message writes (reduces I/O)

Issues Fixed βœ…

πŸ”§ Thread Safety Issue (memory_mixin.py:408-410)

  • Problem: _auto_save_last_index increment outside lock could race
  • Fix Applied: Wrapped increment in _history_lock for consistency
  • Impact: Ensures atomic index updates in concurrent scenarios

πŸ”§ Fallback Logging (session/api.py:355-361)

  • Problem: Silent fallback to append-based persistence
  • Fix Applied: Added warning when set_chat_history() unavailable
  • Impact: Users now know when backend may create duplicates

Core Fix Quality Assessment βœ…

βœ… Root Cause Addressed: Both duplicate sources eliminated

  • Per-turn persist + auto_save sync issues β†’ Fixed via index tracking
  • Repeated save_state() appends β†’ Fixed via atomic replacement

βœ… Test Coverage: Comprehensive regression tests added

  • TestPersistAutoSaveNoDuplicates - Verifies no duplicates from persist+auto_save
  • TestSessionSaveStateNoDuplicates - Verifies idempotent save_state()

βœ… Minimal Impact: Surgical changes, no scope creep

  • Only 3 files modified (+77/-5 lines)
  • No API changes or dependency additions

Final Recommendation: APPROVE βœ…

This PR successfully fixes critical duplicate message bugs with excellent engineering:

  1. High Impact: Fixes data corruption and context bloat issues
  2. Low Risk: Minimal, surgical changes with comprehensive tests
  3. Architecture Compliant: Follows AGENTS.md protocol-driven design
  4. Production Ready: Thread-safe, backward compatible, well-tested

The additional fixes I applied address the valid thread safety and observability concerns raised by CodeRabbit, making this PR even more robust.

Commit pushed to PR: 436a27c8 - Thread safety and logging improvements


Files modified in review fixes:

  • praisonaiagents/agent/memory_mixin.py - Thread-safe index increment
  • praisonaiagents/session/api.py - Fallback behavior logging

- Wrap _auto_save_last_index increment in _history_lock for thread safety
- Add warning when session store lacks set_chat_history (duplicate risk)
- Add debug logging for empty message lists

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
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.

2 participants