fix(session-search): allow scrolling compacted lineage history

This commit is contained in:
Gille 2026-07-26 16:35:23 -06:00 committed by Teknium
parent 5f5afb1eef
commit b93fd077c0
2 changed files with 144 additions and 42 deletions

View File

@ -381,18 +381,98 @@ class TestScrollShape:
assert result["success"] is False
def test_scroll_rejects_current_session_lineage(self, db):
_seed_modpack_sessions(db)
# Grab some valid id from s_oldest
disc = json.loads(session_search(query="modpack", limit=3, db=db))
match = [r for r in disc["results"] if r["session_id"] == "s_oldest"]
if match:
mid = match[0]["match_message_id"]
result = json.loads(session_search(
session_id="s_oldest", around_message_id=mid, db=db,
current_session_id="s_oldest",
))
assert result["success"] is False
assert "current session" in result.get("error", "").lower()
db.create_session("s_current", source="cli")
mid = db.append_message("s_current", role="user", content="still live")
result = json.loads(session_search(
session_id="s_current", around_message_id=mid, db=db,
current_session_id="s_current",
))
assert result["success"] is False
assert "current session" in result.get("error", "").lower()
def test_scroll_allows_compacted_anchor_in_current_session(self, db):
db.create_session("s_current", source="cli")
db.append_message(
"s_current", role="user", content="history removed from live context"
)
db.archive_and_compact("s_current", [
{"role": "assistant", "content": "Compacted history summary"},
])
discovery = json.loads(session_search(
query="history removed", db=db, current_session_id="s_current",
))
assert discovery["count"] == 1
anchor = discovery["results"][0]
result = json.loads(session_search(
session_id=anchor["session_id"],
around_message_id=anchor["match_message_id"],
db=db,
current_session_id="s_current",
))
assert result["success"] is True
assert any(
message["id"] == anchor["match_message_id"]
for message in result["messages"]
)
def test_scroll_allows_compression_ended_parent_from_continuation(self, db):
db.create_session("s_parent", source="cli")
mid = db.append_message(
"s_parent", role="user", content="history summarized into child"
)
db.end_session("s_parent", "compression")
db.create_session("s_current", source="cli", parent_session_id="s_parent")
result = json.loads(session_search(
session_id="s_parent", around_message_id=mid, db=db,
current_session_id="s_current",
))
assert result["success"] is True
assert any(message["id"] == mid for message in result["messages"])
def test_scroll_rejects_active_delegation_child_in_current_lineage(self, db):
db.create_session("s_current", source="cli")
db.create_session(
"s_delegate", source="delegate", parent_session_id="s_current"
)
mid = db.append_message(
"s_delegate", role="assistant", content="live delegated result"
)
result = json.loads(session_search(
session_id="s_delegate", around_message_id=mid, db=db,
current_session_id="s_current",
))
assert result["success"] is False
assert "current session" in result.get("error", "").lower()
def test_scroll_rejects_rewound_anchor_in_compression_parent(self, db):
db.create_session("s_parent", source="cli")
mid = db.append_message(
"s_parent", role="user", content="message removed by rewind"
)
db._conn.execute(
"UPDATE messages SET active = 0, compacted = 0 WHERE id = ?",
(mid,),
)
db._conn.commit()
db.end_session("s_parent", "compression")
db.create_session("s_current", source="cli", parent_session_id="s_parent")
result = json.loads(session_search(
session_id="s_parent", around_message_id=mid, db=db,
current_session_id="s_current",
))
assert result["success"] is False
assert "current session" in result.get("error", "").lower()
def test_scroll_invalid_around_message_id_errors(self, db):
_seed_modpack_sessions(db)

View File

@ -160,6 +160,25 @@ def _is_compression_ended(db, session_id: str) -> bool:
return False
def _get_message_storage_state(db, message_id) -> Optional[Dict[str, Any]]:
"""Return the owning session and visibility flags for *message_id*."""
if not message_id:
return None
try:
with db._lock:
cursor = db._conn.execute(
"SELECT session_id, active, compacted FROM messages WHERE id = ?",
(message_id,),
)
row = cursor.fetchone()
except Exception:
logging.debug(
"message storage-state lookup failed for %s", message_id, exc_info=True
)
return None
return dict(row) if row is not None else None
def _is_compacted_message(db, message_id) -> bool:
"""Return True if *message_id* is a compaction-archived row.
@ -173,18 +192,8 @@ def _is_compacted_message(db, message_id) -> bool:
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, compacted 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 and row["compacted"] == 1
state = _get_message_storage_state(db, message_id)
return state is not None and state["active"] == 0 and state["compacted"] == 1
def _annotate_rebuild_status(db, payload: Dict[str, Any]) -> None:
@ -481,16 +490,40 @@ def _scroll(
window = 5
window = max(1, min(window, 20))
# Reject scrolling inside the active session lineage — those messages are
# already in context.
# Locate the anchor before applying the current-lineage guard. Discovery
# intentionally surfaces two kinds of same-lineage history that are no
# longer in live context: in-place compacted rows, and rows owned by a
# legacy session that ended via compression. Scroll must preserve that
# distinction instead of rejecting the discovery result it just returned.
anchor_state = _get_message_storage_state(db, around_message_id)
owning_session_id = (
anchor_state.get("session_id") if anchor_state is not None else None
)
if current_session_id:
a_root = _resolve_lineage(db, session_id)
anchor_session_id = owning_session_id or session_id
a_root = _resolve_lineage(db, anchor_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)",
success=False,
is_compacted_anchor = (
anchor_state is not None
and anchor_state["active"] == 0
and anchor_state["compacted"] == 1
)
is_inactive_non_compacted_anchor = (
anchor_state is not None
and anchor_state["active"] == 0
and anchor_state["compacted"] != 1
)
is_compression_history = (
not is_inactive_non_compacted_anchor
and _is_compression_ended(db, anchor_session_id)
)
if not (is_compacted_anchor or is_compression_history):
return tool_error(
"scroll rejected: anchor lives in the current session lineage (already in your active context)",
success=False,
)
# Session existence check
try:
@ -515,18 +548,7 @@ def _scroll(
# child sessions). Locate the real owning session and refetch.
rebind_warning = None
if not messages:
owning = None
try:
conn = getattr(db, "_conn", None)
if conn is not None:
row = conn.execute(
"SELECT session_id FROM messages WHERE id = ?",
(around_message_id,),
).fetchone()
owning = row[0] if row else None
except Exception as e:
logging.debug("owning-session lookup failed: %s", e, exc_info=True)
owning = None
owning = owning_session_id
if owning and owning != session_id:
a_root = _resolve_lineage(db, session_id)
o_root = _resolve_lineage(db, owning)