From 54d099f29cf1a0a7556589d758f4d9d6620823ed Mon Sep 17 00:00:00 2001 From: pyrate-llama Date: Tue, 7 Apr 2026 02:43:55 -0700 Subject: [PATCH] feat(hindsight): add on_pre_compress hook to preserve context before compression When Hermes compresses its context window, the original messages are summarised and discarded. This hook fires just before compression, capturing the last 10 user/assistant messages and persisting them to the Hindsight knowledge graph via retain(). Messages are tagged with '[Pre-compression context]' for easy identification and are retained with context='pre_compress' so they remain searchable via recall/reflect. The retain call runs in a background thread so it never blocks the compression pipeline. Closes vectorize-io/hindsight#870 (migrated from standalone plugin). --- plugins/memory/hindsight/__init__.py | 49 +++++++++++++++++++++++++++- plugins/memory/hindsight/plugin.yaml | 1 + 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/plugins/memory/hindsight/__init__.py b/plugins/memory/hindsight/__init__.py index c39679b73c8f6..789e932c55e89 100644 --- a/plugins/memory/hindsight/__init__.py +++ b/plugins/memory/hindsight/__init__.py @@ -225,6 +225,7 @@ def __init__(self): self._bank_mission = "" self._bank_retain_mission: str | None = None self._retain_async = True + self._precompress_thread = None @property def name(self) -> str: @@ -851,7 +852,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: def shutdown(self) -> None: logger.debug("Hindsight shutdown: waiting for background threads") global _loop, _loop_thread - for t in (self._prefetch_thread, self._sync_thread): + for t in (self._prefetch_thread, self._sync_thread, self._precompress_thread): if t and t.is_alive(): t.join(timeout=5.0) if self._client is not None: @@ -877,6 +878,52 @@ def shutdown(self) -> None: _loop = None _loop_thread = None + def on_pre_compress(self, messages: list[dict[str, Any]] | None = None, **kwargs) -> str: + """Extract and retain messages before context window compression discards them. + + When Hermes compresses the context window to save tokens, the original + messages are summarised and the full text is lost. This hook fires just + before that happens, giving us a chance to persist the about-to-be-discarded + turns into the Hindsight knowledge graph so they remain searchable via + recall/reflect. + + The retain call runs in a background thread so it never blocks the + compression pipeline. + """ + if not messages or not self._bank_id: + return "" + + parts: list[str] = [] + for msg in messages[-10:]: + role = msg.get("role", "") + content = msg.get("content", "") + if isinstance(content, str) and content.strip() and role in ("user", "assistant"): + parts.append(f"{role}: {content[:500]}") + if not parts: + return "" + combined = "\n".join(parts) + + def _flush() -> None: + try: + client = self._get_client() + _run_sync(client.aretain( + bank_id=self._bank_id, + content=f"[Pre-compression context]\n{combined}", + context="pre_compress", + )) + logger.info( + "Hindsight pre-compression flush: %d messages retained", + len(parts), + ) + except Exception as exc: + logger.warning("Hindsight pre-compression flush failed: %s", exc) + + if self._precompress_thread and self._precompress_thread.is_alive(): + self._precompress_thread.join(timeout=5.0) + self._precompress_thread = threading.Thread(target=_flush, daemon=True, name="hindsight-precompress") + self._precompress_thread.start() + return "" + def register(ctx) -> None: """Register Hindsight as a memory provider plugin.""" diff --git a/plugins/memory/hindsight/plugin.yaml b/plugins/memory/hindsight/plugin.yaml index b12c09142bb6b..4fe38dd72b31b 100644 --- a/plugins/memory/hindsight/plugin.yaml +++ b/plugins/memory/hindsight/plugin.yaml @@ -6,3 +6,4 @@ pip_dependencies: requires_env: [] hooks: - on_session_end + - on_pre_compress