fix(search): surface compaction-archived and compression-parent sessions in discovery

After context compaction, pre-compaction content was invisible to
session_search — a memory black hole. The _discover() skip logic
filtered both same-session and same-lineage hits unconditionally,
without distinguishing compression-summarised content (gone from live
context) from delegation children (still visible to the parent agent).

Reworked _resolve_to_parent to return (root_id, has_compression_hop),
checking end_reason='compression' on every hop during the same
db.get_session() traversal — zero extra queries.

_discover() now has three compression-aware paths:
- In-place compaction: FTS hits on active=0 (compacted=1) rows pass
  through even when raw_sid == current_session_id
- Legacy rotation: lineage hits pass through when has_compression_hop
  is true on either side of the chain
- Delegation children: still excluded (no compression edge)

18 new tests covering all three scenarios + unit tests for the helpers.

Addresses Teknium's review feedback on #6256.
Closes #13840, #13841.
This commit is contained in:
MacroAnarchy 2026-07-12 12:41:18 +02:00 • committed by Teknium
parent 8c745314b9
commit 711f1c2f1a
2 changed files with 316 additions and 16 deletions

View file

@ -17,6 +17,8 @@ from tools.session_search_tool import (
SESSION_SEARCH_SCHEMA,
_HIDDEN_SESSION_SOURCES,
_format_timestamp,
_is_compacted_message,
_resolve_to_parent,
session_search,
)
@ -772,3 +774,241 @@ class TestCompactionSummaryFiltering:
entry = result["results"][0]
for msg in entry.get("bookend_start", []):
assert "[CONTEXT SUMMARY]" not in (msg.get("content") or "")
# =========================================================================
# Compression-aware discovery (#6256)
#
# After compression (in-place compaction or legacy rotation), pre-compaction
# content is no longer in the live context but MUST stay discoverable via
# session_search. The old code skipped any FTS hit on the current session or
# lineage, creating a "memory black hole". Delegation children must STAY
# excluded — their content is still visible to the parent agent.
# =========================================================================
class TestResolveToParent:
"""Unit tests for _resolve_to_parent's compression-aware tuple return."""
def test_root_session_no_compression(self, db):
db.create_session("s1", source="cli")
root, has_compression = _resolve_to_parent(db, "s1")
assert root == "s1"
assert has_compression is False
def test_empty_session_id(self, db):
root, has_compression = _resolve_to_parent(db, "")
assert root == ""
assert has_compression is False
def test_none_session_id(self, db):
root, has_compression = _resolve_to_parent(db, None)
assert root is None
assert has_compression is False
def test_legacy_rotation_detects_compression(self, db):
"""Parent ended with end_reason='compression', child has parent_session_id."""
db.create_session("s_parent", source="cli")
db.end_session("s_parent", "compression")
db.create_session("s_child", source="cli", parent_session_id="s_parent")
root, has_compression = _resolve_to_parent(db, "s_child")
assert root == "s_parent"
assert has_compression is True
def test_delegation_no_compression(self, db):
"""Delegation child: parent_session_id set but no compression end_reason."""
db.create_session("s_parent", source="cli")
db.create_session("s_child", source="cli", parent_session_id="s_parent")
root, has_compression = _resolve_to_parent(db, "s_child")
assert root == "s_parent"
assert has_compression is False
def test_multi_level_compression_chain(self, db):
"""Grandparent → parent → child, both with compression edges."""
db.create_session("s_gp", source="cli")
db.end_session("s_gp", "compression")
db.create_session("s_p", source="cli", parent_session_id="s_gp")
db.end_session("s_p", "compression")
db.create_session("s_c", source="cli", parent_session_id="s_p")
root, has_compression = _resolve_to_parent(db, "s_c")
assert root == "s_gp"
assert has_compression is True
def test_chain_with_mixed_edges(self, db):
"""Compression parent → delegation-style child (no end_reason on child)."""
db.create_session("s_gp", source="cli")
db.end_session("s_gp", "compression")
db.create_session("s_p", source="cli", parent_session_id="s_gp")
# s_p does NOT end with compression — but ancestor s_gp does
db.create_session("s_c", source="cli", parent_session_id="s_p")
root, has_compression = _resolve_to_parent(db, "s_c")
assert root == "s_gp"
assert has_compression is True
class TestIsCompactedMessage:
"""Unit tests for the _is_compacted_message helper."""
def test_active_message_returns_false(self, db):
db.create_session("s1", source="cli")
mid = db.append_message("s1", role="user", content="hello")
assert _is_compacted_message(db, mid) is False
def test_compacted_message_returns_true(self, db):
db.create_session("s1", source="cli")
mid = db.append_message("s1", role="user", content="archived content")
db.archive_and_compact("s1", [
{"role": "assistant", "content": "compacted summary"},
])
# mid is now active=0, compacted=1
assert _is_compacted_message(db, mid) is True
def test_none_message_id(self, db):
assert _is_compacted_message(db, None) is False
def test_nonexistent_message_id(self, db):
assert _is_compacted_message(db, 999999) is False
class TestInPlaceCompactionDiscovery:
"""In-place compaction: archived turns on the SAME session_id must be
discoverable from the current session."""
def test_archived_content_discoverable_after_compaction(self, db):
"""The core regression: pre-compaction content on the current session
must surface in discovery even though raw_sid == current_session_id."""
db.create_session("s_compact", source="cli")
db.append_message("s_compact", role="user",
content="The spectral phoenix only spawns during full moons")
db.append_message("s_compact", role="assistant",
content="Spectral phoenix requires moonstone bait")
db.archive_and_compact("s_compact", [
{"role": "user", "content": "Summary: spectral phoenix discussed"},
{"role": "assistant", "content": "Acknowledged spectral phoenix info"},
])
result = json.loads(session_search(
query="spectral phoenix", db=db, current_session_id="s_compact",
))
assert result["success"] is True
assert result["count"] >= 1
# The hit should be from the same session (archived rows)
hit = result["results"][0]
assert hit["session_id"] == "s_compact"
def test_live_content_still_filtered_on_current_session(self, db):
"""Non-compacted (active) content on the current session stays filtered."""
db.create_session("s_live", source="cli")
db.append_message("s_live", role="user", content="crystal golem farming route")
result = json.loads(session_search(
query="crystal golem", db=db, current_session_id="s_live",
))
assert result["count"] == 0
def test_mixed_active_and_compacted_on_same_session(self, db):
"""A session that has been compacted: the archived content is
discoverable, but the new (post-compaction) active content is not
(it's in live context)."""
db.create_session("s_mixed", source="cli")
# Pre-compaction content (will be archived)
db.append_message("s_mixed", role="user", content="ancient ruins exploration log")
db.append_message("s_mixed", role="assistant", content="ancient ruins mapped")
# Compact
db.archive_and_compact("s_mixed", [
{"role": "user", "content": "Summary of ancient ruins exploration"},
{"role": "assistant", "content": "Continuing ancient ruins work"},
])
# Archived content should be discoverable
result_archived = json.loads(session_search(
query="ancient ruins exploration", db=db,
current_session_id="s_mixed",
))
assert result_archived["count"] >= 1
class TestLegacyRotationDiscovery:
"""Legacy rotation: parent session ended with end_reason='compression',
child session created. Parent's pre-compaction content must be discoverable
from the child."""
def test_compression_parent_discoverable_from_child(self, db):
db.create_session("s_parent", source="cli")
db.append_message("s_parent", role="user",
content="The void crystal mining requires diamond pickaxe")
db.append_message("s_parent", role="assistant",
content="Void crystal found in the deep caverns")
db.end_session("s_parent", "compression")
db.create_session("s_child", source="cli", parent_session_id="s_parent")
db.append_message("s_child", role="user", content="Continue void crystal work")
result = json.loads(session_search(
query="void crystal", db=db, current_session_id="s_child",
))
assert result["success"] is True
assert result["count"] >= 1
sids = [r["session_id"] for r in result["results"]]
assert "s_parent" in sids
def test_multi_level_compression_chain_discoverable(self, db):
"""Grandparent → parent → child, each compression-rotated. Content from
ancestors must be discoverable."""
db.create_session("s_gp", source="cli")
db.append_message("s_gp", role="user",
content="Project titan initial architecture design")
db.end_session("s_gp", "compression")
db.create_session("s_p", source="cli", parent_session_id="s_gp")
db.append_message("s_p", role="user",
content="Project titan second phase planning")
db.end_session("s_p", "compression")
db.create_session("s_c", source="cli", parent_session_id="s_p")
db.append_message("s_c", role="user", content="Project titan final review")
result = json.loads(session_search(
query="project titan", db=db, current_session_id="s_c",
))
assert result["count"] >= 1
# Should find content from s_gp or s_p (or both, deduped by lineage)
sids = [r["session_id"] for r in result["results"]]
assert any(s in ("s_gp", "s_p") for s in sids)
class TestDelegationExclusion:
"""Delegation children (delegate_task) must STAY excluded — their content
is still visible to the parent agent. parent_session_id is set but the
parent does NOT have end_reason='compression'."""
def test_delegation_parent_excluded_from_child(self, db):
"""Child can see its own content but parent's live content stays
excluded (it's in context via delegation)."""
db.create_session("s_parent", source="cli")
db.append_message("s_parent", role="user",
content="nebula deployment infrastructure setup")
db.append_message("s_parent", role="assistant",
content="Nebula deployment configured successfully")
db.create_session("s_child", source="cli", parent_session_id="s_parent")
db.append_message("s_child", role="user",
content="delegated nebula deployment subtask")
result = json.loads(session_search(
query="nebula deployment", db=db, current_session_id="s_child",
))
assert result["count"] == 0
def test_delegation_child_excluded_from_parent(self, db):
"""Parent searching should not see delegation child content either —
both are in the same lineage with no compression edge."""
db.create_session("s_parent", source="cli")
db.append_message("s_parent", role="user",
content="Working on stellar forge project")
db.create_session("s_child", source="cli", parent_session_id="s_parent")
db.append_message("s_child", role="user",
content="stellar forge delegated subtask execution")
result = json.loads(session_search(
query="stellar forge", db=db, current_session_id="s_parent",
))
assert result["count"] == 0

View file

@ -99,19 +99,31 @@ def _is_compaction_summary(content: str) -> bool:
return any(stripped.startswith(p) for p in _COMPACTION_PREFIXES)
def _resolve_to_parent(db, session_id: str) -> tuple[str, bool]:
"""Walk parent_session_id chain to the lineage root.
def _resolve_to_parent(db, session_id: str) -> str:
"""Walk parent_session_id chain to the lineage root. Falls back to input on errors."""
Returns ``(root_id, has_compression_hop)`` where ``has_compression_hop`` is
True if any session along the chain ended with ``end_reason = 'compression'``
— i.e. at least one parent/ancestor was compression-rotated into this
lineage. That flag lets callers distinguish a compression-split lineage
(parent content summarised away, no longer in live context) from a
delegation lineage (child content still visible to the parent agent).
Falls back to ``(session_id, False)`` on errors.
"""
if not session_id:
return session_id
visited = set()
return session_id, False
visited: set[str] = set()
cur = session_id
has_compression = False
while cur and cur not in visited:
visited.add(cur)
try:
s = db.get_session(cur)
if not s:
break
if s.get("end_reason") == "compression":
has_compression = True
parent = s.get("parent_session_id")
if not parent:
break
@ -119,7 +131,35 @@ def _resolve_to_parent(db, session_id: str) -> str:
except Exception as e:
logging.debug("Error resolving parent for %s: %s", cur, e, exc_info=True)
break
return cur
return cur, has_compression
def _resolve_lineage(db, session_id: str) -> str:
"""Convenience: return only the lineage root (ignores compression hop)."""
return _resolve_to_parent(db, session_id)[0]
def _is_compacted_message(db, message_id) -> bool:
"""Return True if *message_id* is a soft-archived compaction row (active=0).
Used by ``_discover`` to distinguish a compaction-archived FTS hit on the
current session (pre-compaction content no longer in live context — should
stay discoverable) from an active live hit (already in context — skip).
Returns False on any error so the caller falls back to the safe default
(skip the current session).
"""
if not message_id:
return False
try:
with db._lock:
cursor = db._conn.execute(
"SELECT active FROM messages WHERE id = ?", (message_id,)
)
row = cursor.fetchone()
except Exception:
logging.debug("is_compacted_message lookup failed for %s", message_id, exc_info=True)
return False
return row is not None and row["active"] == 0
def _annotate_rebuild_status(db, payload: Dict[str, Any]) -> None:
@ -328,7 +368,7 @@ def _list_recent_sessions(db, limit: int, current_session_id: str = None) -> str
order_by_last_active=True,
) # fetch extra so we can skip current
current_root = _resolve_to_parent(db, current_session_id) if current_session_id else None
current_root = _resolve_lineage(db, current_session_id) if current_session_id else None
results = []
for s in sessions:
@ -395,8 +435,8 @@ def _scroll(
# Reject scrolling inside the active session lineage — those messages are
# already in context.
if current_session_id:
a_root = _resolve_to_parent(db, session_id)
c_root = _resolve_to_parent(db, current_session_id)
a_root = _resolve_lineage(db, session_id)
c_root = _resolve_lineage(db, current_session_id)
if a_root and c_root and a_root == c_root:
return tool_error(
"scroll rejected: anchor lives in the current session lineage (already in your active context)",
@ -439,8 +479,8 @@ def _scroll(
logging.debug("owning-session lookup failed: %s", e, exc_info=True)
owning = None
if owning and owning != session_id:
a_root = _resolve_to_parent(db, session_id)
o_root = _resolve_to_parent(db, owning)
a_root = _resolve_lineage(db, session_id)
o_root = _resolve_lineage(db, owning)
if a_root and o_root and a_root == o_root:
try:
rebind_view = db.get_messages_around(owning, around_message_id, window=window)
@ -509,7 +549,7 @@ def _title_match_result(
if not session_id:
return None
lineage_root = _resolve_to_parent(db, session_id)
lineage_root = _resolve_lineage(db, session_id)
if current_lineage_root and lineage_root == current_lineage_root:
return None
@ -568,7 +608,9 @@ def _discover(
) -> str:
"""Discovery shape: FTS5 + anchored window + bookends per hit. Single call."""
role_list = role_filter if role_filter else ["user", "assistant"]
current_lineage_root = _resolve_to_parent(db, current_session_id) if current_session_id else None
current_lineage_root, current_has_compression = (
_resolve_to_parent(db, current_session_id) if current_session_id else (None, False)
)
title_result = _title_match_result(db, query, current_lineage_root)
try:
@ -620,12 +662,30 @@ def _discover(
if len(seen_sessions) >= limit:
break
raw_sid = r["session_id"]
resolved_sid = _resolve_to_parent(db, raw_sid)
# Skip the current session lineage
resolved_sid, has_compression = _resolve_to_parent(db, raw_sid)
# Skip the current session lineage — UNLESS the content has been
# compression-summarised out of the live context (memory black hole
# after compression). Two sub-cases:
#
# Legacy rotation: the FTS hit lives in a session whose lineage chain
# has a compression edge (end_reason='compression' on an ancestor).
# The parent's pre-compaction content is gone from the active context,
# so it must stay discoverable.
#
# In-place compaction: the FTS hit lives on the SAME session_id as the
# current session, but the matched message row is an archived
# (active=0, compacted=1) row. The live-context load filters active=1,
# so that content is no longer in context — let it through.
is_compacted_hit = _is_compacted_message(db, r.get("id"))
if current_lineage_root and resolved_sid == current_lineage_root:
continue
if not (has_compression or current_has_compression or is_compacted_hit):
continue
if current_session_id and raw_sid == current_session_id:
continue
# Same-session hit: only skip if the matched message is still live
# (active=1). Archived/compacted rows are pre-compaction content
# that's been summarised away — let them through.
if not is_compacted_hit:
continue
if resolved_sid not in seen_sessions:
row = dict(r)
row["_lineage_root"] = resolved_sid