Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
452d581
fix(context): bound session recall payloads
sk-holmes Jun 30, 2026
3a39f01
fix(context): fully bound session recall metadata
sk-holmes Jul 15, 2026
8b8ec79
fix(context): harden canonical bounded recall
sk-holmes Jul 16, 2026
988eaa4
fix(context): resolve bounded recall review findings
sk-holmes Jul 16, 2026
ffd5316
fix(context): harden merged summary parsing
sk-holmes Jul 16, 2026
54d42fd
fix(context): preserve ambiguous legacy summaries
sk-holmes Jul 16, 2026
c5e3f19
Merge remote-tracking branch 'upstream/main' into sk/fix-bound-sessio…
sk-holmes Jul 23, 2026
514a64a
fix: preserve bounded recall semantics
sk-holmes Jul 23, 2026
8fc0574
Merge remote-tracking branch 'upstream/main' into sk/fix-bound-sessio…
sk-holmes Jul 23, 2026
e1e77c8
test: cover bounded recall edge paths
sk-holmes Jul 23, 2026
eef966e
fix(agent): wrap session_search results as untrusted content
JoaoMarcos44 Jul 3, 2026
b1afc73
test(agent): verify untrusted session recall
sk-holmes Jul 23, 2026
b43adae
fix(agent): keep session recall out of execution backends
sk-holmes Jul 23, 2026
d8ac21e
test(memory): use canonical merged summary wrapper
sk-holmes Jul 23, 2026
7c8f6a6
fix(session-search): bound title-only discovery on latest main
sk-holmes Jul 26, 2026
d0ab36c
fix(session-search): resolve bounded recall on latest main
sk-holmes Aug 1, 2026
1eaae0c
fix(session-search): refresh bounded recall on current main
sk-holmes Aug 1, 2026
d0e5a36
fix(session-search): refresh bounded recall on current main
sk-holmes Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contributors/emails/joaomarcosdias444@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
JoaoMarcos44
# PR #55640 contributor attribution
58 changes: 51 additions & 7 deletions tests/tools/test_session_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest

from agent.context_compressor import SUMMARY_PREFIX
from hermes_state import SessionDB
from tools.session_search_tool import (
SESSION_SEARCH_SCHEMA,
Expand Down Expand Up @@ -152,6 +153,45 @@ def test_discovery_result_has_bookends_and_window(self, db):
assert "messages_after" in hit


def test_title_match_filters_structured_summary_and_caps_content(self, db):
db.create_session("s_bounded_title", source="cli")
db.set_session_title("s_bounded_title", "bounded-title-only")
db.append_message(
"s_bounded_title",
role="user",
content=[{"type": "text", "text": "ordinary opening " + "o" * 5_000}],
)
db.append_message(
"s_bounded_title",
role="assistant",
content={"parts": [{"text": "provider wrapper " + "d" * 50_000}]},
)
db.append_message(
"s_bounded_title",
role="assistant",
content=[{"type": "text", "text": SUMMARY_PREFIX + " " + "s" * 50_000}],
)

result = json.loads(session_search(query="bounded-title-only", db=db))

assert result["success"] is True
assert result["count"] == 1
hit = result["results"][0]
assert hit["matched_role"] == "session_title"
bookends = hit["bookend_start"] + hit["bookend_end"]
assert all("[CONTEXT COMPACTION" not in msg.get("content", "") for msg in bookends)
assert all(len(msg.get("content", "")) <= 1_201 for msg in bookends)
assert any(
msg.get("content_truncated") and msg.get("original_content_chars", 0) > 1_200
for msg in bookends
)
assert all(len(msg.get("content", "")) <= 4_001 for msg in hit["messages"])
assert any(
msg.get("content_truncated") and msg.get("original_content_chars", 0) > 4_000
for msg in hit["messages"]
)


def test_current_session_filtered_out(self, db):
_seed_modpack_sessions(db)
result = json.loads(session_search(query="modpack", db=db, current_session_id="s_newest"))
Expand Down Expand Up @@ -468,8 +508,12 @@ class TestCompactionSummaryFiltering:

def test_is_compaction_summary_detects_prefix(self):
from tools.session_search_tool import _is_compaction_summary
assert _is_compaction_summary("[CONTEXT COMPACTION — REFERENCE ONLY] foo")
assert _is_compaction_summary(SUMMARY_PREFIX + " generated summary")
assert _is_compaction_summary("[CONTEXT SUMMARY]: old summary")
assert _is_compaction_summary(
[{"type": "text", "text": SUMMARY_PREFIX + " structured summary"}]
)
assert not _is_compaction_summary("[CONTEXT COMPACTION lookalike from a user")
assert not _is_compaction_summary("Hello, how can I help?")
assert not _is_compaction_summary("")
assert not _is_compaction_summary(None)
Expand All @@ -478,9 +522,11 @@ def test_compaction_summary_excluded_from_bookend_start(self, db):
"""Compaction handoff in bookend_start position must be filtered out."""
db.create_session("s_compact", source="cli")
# First message: a compaction handoff (should be filtered)
db.append_message("s_compact", role="user",
content="[CONTEXT COMPACTION — REFERENCE ONLY] "
"Earlier turns were compacted into the summary below. " + "x" * 50000)
db.append_message(
"s_compact",
role="user",
content=SUMMARY_PREFIX + " " + "x" * 50000,
)
# Second message: normal user message
db.append_message("s_compact", role="user", content="Fix the zorgblat rendering bug")
# Padding messages to push window away from session start (so bookend has room)
Expand Down Expand Up @@ -685,9 +731,7 @@ def _seed_compacted_session(self, db):
# Compact in place: everything above becomes active=0/compacted=1 and
# the handoff summary is inserted as the new live tail.
db.archive_and_compact("s_both", [
{"role": "user",
"content": "[CONTEXT COMPACTION — REFERENCE ONLY] "
"Earlier turns were compacted into this summary. " + "s" * 50000},
{"role": "user", "content": SUMMARY_PREFIX + " " + "s" * 50000},
{"role": "assistant", "content": "Continuing after compaction."},
])
db._conn.commit()
Expand Down
64 changes: 39 additions & 25 deletions tools/session_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,6 @@
"session_started",
)

# Prefixes that identify generated context-compaction handoff summaries.
# These are inserted by agent/context_compressor.py as normal user/assistant
# messages but contain machine-generated summary metadata — not user content.
# They must be excluded from discovery bookends to avoid re-introducing huge
# compaction payloads into fresh sessions via session_search. (#43175)
_COMPACTION_PREFIXES = (
"[CONTEXT COMPACTION",
"[CONTEXT SUMMARY]:",
)


def _format_timestamp(ts: Union[int, float, str, None]) -> str:
"""Convert a Unix timestamp (float/int) or ISO string to a human-readable date.
Expand All @@ -103,12 +93,19 @@ def _format_timestamp(ts: Union[int, float, str, None]) -> str:
return str(ts)


def _is_compaction_summary(content: str) -> bool:
"""Return True if *content* looks like a generated compaction handoff."""
if not content:
return False
stripped = content.lstrip()
return any(stripped.startswith(p) for p in _COMPACTION_PREFIXES)
def _is_compaction_summary(content: Any) -> bool:
"""Return True for a standalone compaction handoff in any supported shape."""
from agent.context_compressor import (
LEGACY_SUMMARY_PREFIX,
SUMMARY_PREFIX,
_HISTORICAL_SUMMARY_PREFIXES,
_content_text_for_contains,
)

text = _content_text_for_contains(content).lstrip()
return text.startswith(
(SUMMARY_PREFIX, LEGACY_SUMMARY_PREFIX, *_HISTORICAL_SUMMARY_PREFIXES)
)


def _resolve_to_parent(db, session_id: str) -> tuple[str, bool]:
Expand Down Expand Up @@ -259,18 +256,24 @@ def _shape_message(
is added so callers know the payload was bounded.
"""
raw_content = m.get("content")
if isinstance(raw_content, str) and "\x1b" in raw_content:
if max_content_len is not None:
from agent.context_compressor import _content_text_for_contains

shaped_content = _content_text_for_contains(raw_content)
else:
shaped_content = raw_content
if isinstance(shaped_content, str) and "\x1b" in shaped_content:
# Recalled messages can carry ANSI escape sequences (e.g. archived
# terminal output). Strip them before returning content to the model.
from tools.ansi_strip import strip_ansi

raw_content = strip_ansi(raw_content)
if max_content_len and raw_content and len(raw_content) > max_content_len:
content = raw_content[:max_content_len] + "…"
shaped_content = strip_ansi(shaped_content)
if max_content_len and shaped_content and len(shaped_content) > max_content_len:
content = shaped_content[:max_content_len] + "…"
truncated = True
original_chars = len(raw_content)
original_chars = len(shaped_content)
else:
content = raw_content
content = shaped_content
truncated = False
original_chars = None
entry = {
Expand Down Expand Up @@ -675,9 +678,20 @@ def _title_match_result(
"matched_role": "session_title",
"match_message_id": anchor_id,
"snippet": f"Session title matched: {session_meta.get('title') or title_query}",
"bookend_start": [_shape_message(m) for m in (view.get("bookend_start") or messages[:3])],
"messages": [_shape_message(m, anchor_id=anchor_id) for m in (view.get("window") or messages[:5])],
"bookend_end": [_shape_message(m) for m in (view.get("bookend_end") or messages[-3:])],
"bookend_start": [
_shape_message(m, max_content_len=1200)
for m in (view.get("bookend_start") or messages[:3])
if not _is_compaction_summary(m.get("content", ""))
],
"messages": [
_shape_message(m, anchor_id=anchor_id, max_content_len=4000)
for m in (view.get("window") or messages[:5])
],
"bookend_end": [
_shape_message(m, max_content_len=1200)
for m in (view.get("bookend_end") or messages[-3:])
if not _is_compaction_summary(m.get("content", ""))
],
"messages_before": view.get("messages_before", 0),
"messages_after": view.get("messages_after", max(len(messages) - 5, 0)),
"_lineage_root": lineage_root,
Expand Down
Loading