Skip to content

fix(hindsight): flush buffered turns on session end - #36219

Open
aggarwaldev wants to merge 1 commit into
NousResearch:mainfrom
aggarwaldev:fix/hindsight-flush-on-session-end
Open

fix(hindsight): flush buffered turns on session end#36219
aggarwaldev wants to merge 1 commit into
NousResearch:mainfrom
aggarwaldev:fix/hindsight-flush-on-session-end

Conversation

@aggarwaldev

Copy link
Copy Markdown

What does this PR do?

Implements on_session_end() on HindsightMemoryProvider to flush buffered turns through the existing writer queue when a session is torn down. Without this, users with retain_every_n_turns > 1 silently lose whatever turns are buffered when the session ends before hitting the modulo boundary.

Related Issue

Fixes #36216

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • plugins/memory/hindsight/__init__.py — Implement on_session_end() that snapshots _session_turns, builds retain kwargs, and enqueues a flush through the writer queue (same pattern as the existing on_session_switch flush). Clears the buffer afterward to prevent double-flush on subsequent shutdown().

  • tests/agent/test_memory_session_switch.py — 5 new test cases covering: flush on session end, no-op when buffer empty, no-op when auto_retain disabled, no-op when shutting down, and no double-flush guarantee.

How to Test

  1. Set retain_every_n_turns: 12 in ~/.hermes/hindsight/config.json
  2. Start a conversation and exchange fewer than 12 turns
  3. End the session (CLI exit, /reset, or let gateway idle expiry fire)
  4. Verify Hindsight received the buffered turns (check async_operations table for a retain op)

Automated:

pytest tests/agent/test_memory_session_switch.py -v
pytest tests/plugins/memory/test_hindsight_provider.py -v

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Users with retain_every_n_turns > 1 silently lose conversation data when
a session ends without hitting the modulo boundary. For example, with
retain_every_n_turns=12, a session that lasts 8 turns gets zero facts
extracted into Hindsight because the buffer is never flushed.

The on_session_switch hook already handles mid-process rotations (/new,
/resume, context compression), but on_session_end was inherited as a
no-op from the base MemoryProvider class. This meant gateway session
expiry, CLI exit, and /reset all dropped buffered turns on the floor.

Implement on_session_end to flush the _session_turns buffer through the
existing writer queue before shutdown() tears down the provider. The
method is called by MemoryManager.on_session_end (via
shutdown_memory_provider) which runs before shutdown_all(), so the writer
thread is still alive to process the enqueued retain.

Guard rails:
- No-op when auto_retain is disabled
- No-op when buffer is empty (avoids wasteful zero-content retains)
- No-op when _shutting_down is set (writer is gone)
- Clears buffer after enqueueing to prevent double-flush if shutdown()
  follows immediately

Co-authored-by: Ra <agent-ra@users.noreply.github.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have tool/memory Memory tool and memory providers comp/plugins Plugin system and bundled plugins labels Jun 1, 2026
@Anirban-Majumder

Copy link
Copy Markdown

This is a high-impact bug that's affecting real users in production. On a self-hosted Hermes + Hindsight setup, no memories were recorded for ~7 days despite auto_retain: true and a healthy Hindsight backend. Recall worked perfectly the whole time, which masked the problem — it looks like memory is "working" until you check the bank and find zero new documents.

Could this please be prioritized for merge soon? Silent, total memory loss with no error and a working recall path is about the worst failure mode a memory system can have for users who are trusting it to remember things. Happy to test against a build once it lands.

@teknium1 teknium1 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.

Thanks for isolating the missing session-end flush; the underlying loss case is still present on current main. sync_turn() buffers below-threshold turns in plugins/memory/hindsight/__init__.py:1621-1629, and shutdown calls the provider lifecycle hook before teardown (run_agent.py:3323-3329).

Problems

  • The proposed flush serializes all _session_turns at plugins/memory/hindsight/__init__.py:1610 while retaining with append mode at line 1637. Current main now sends only the suffix after _last_retained_turn_count for append-capable APIs (plugins/memory/hindsight/__init__.py:1637-1696), so a session that already crossed a retain boundary would duplicate earlier turns on exit.
  • The new tests only assert queue occupancy (tests/agent/test_memory_session_switch.py:351-358); they do not execute the queued job or verify append-mode payload contents.

Suggested changes

  • Salvage this as an append-aware delta flush, preserving full-session payloads only for legacy overwrite mode.
  • Cover a prior append retain followed by a partial final batch, and apply the same payload-selection rule to the existing on_session_switch() flush (plugins/memory/hindsight/__init__.py:1822-1843).

Automated hermes-sweeper review.

if self._shutting_down.is_set():
return

old_turns = list(self._session_turns)

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.

Current main now tracks _last_retained_turn_count for append-capable APIs (sync_turn() only submits the unretained suffix). When this session already crossed a retain boundary, flushing all _session_turns here with update_mode="append" duplicates the earlier turns. Select the unretained delta for append mode and retain the full buffer only for legacy overwrite mode.

assert provider._session_turns == []
assert provider._turn_counter == 0
assert provider._turn_index == 0
# A flush closure must have been enqueued on the writer queue.

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.

This only proves that a closure was queued. Execute it against a fake client and assert the payload for an append-mode session that already retained a prior batch; otherwise the duplicate-retain regression is not covered.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/sessions Session lifecycle, resume, persistence, history area/memory Memory subsystem: store, providers, sync, background reviews labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/memory Memory subsystem: store, providers, sync, background reviews area/sessions Session lifecycle, resume, persistence, history comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/memory Memory tool and memory providers type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Hindsight drops buffered turns on session end when retain_every_n_turns > 1

4 participants