fix(sessions): a pinned conversation can't be paged out of the list

`list_sessions_rich` returns one recency-ordered window, so a pinned
conversation that hadn't been touched in a while simply wasn't in the
payload. The desktop's Pinned section resolves pins against the loaded
rows, so the pin rendered as nothing until something dragged the row
back onto the page.

A pin is a "this must always be reachable" statement, which makes
falling off the page a bug rather than a paging outcome. `include_pinned`
adds one bounded query for the rows carrying `pinned = 1` that the
window missed, reusing the page's own WHERE clause — an archived or
filtered-out conversation stays out, and a pin is never a filter bypass.
It runs before compression projection, so a back-filled root surfaces
under its live tip exactly like a row that made the page on its own.

Co-authored-by: hrnbld <260600092+hrnbld@users.noreply.github.com>
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Co-authored-by: Tamaz-sujashvili <56168197+Tamaz-sujashvili@users.noreply.github.com>
Co-authored-by: ferminquant <14808645+ferminquant@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-29 12:00:21 -05:00
parent 1fe06115d1
commit 1b317d23fb
2 changed files with 144 additions and 0 deletions

View file

@ -6491,6 +6491,7 @@ class SessionDB:
id_query: str = None,
search_query: str = None,
compact_rows: bool = False,
include_pinned: bool = False,
) -> List[Dict[str, Any]]:
"""List sessions with preview (first user message) and last active timestamp.
@ -6530,6 +6531,14 @@ class SessionDB:
the SELECT so SQLite never copies it out of the B-tree page a
significant I/O saving on large databases where the blob routinely
runs to tens of kilobytes per row.
Pass ``include_pinned=True`` to back-fill any conversation carrying the
durable ``pinned`` flag that the LIMIT/OFFSET window left out. A pin is
a "this must always be reachable" statement, so a pinned conversation
aging past the requested page is a bug, not a paging outcome the
desktop sidebar would render an empty Pinned section. Back-filled rows
obey the same filters (source, archived, min_message_count) as the
page: an archived or filtered-out conversation stays out.
"""
# Rows carry token/cost totals — drain queued deltas first so
# listings (sidebar, /resume, dashboards) show exact counters.
@ -6577,6 +6586,9 @@ class SessionDB:
where_clauses.append("s.archived = 0")
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
# Snapshot the filter params before the query builders below extend
# them with LIMIT/OFFSET — the pinned back-fill reuses the same WHERE.
base_where_params = list(params)
# Optional session-id filter, pushed into SQL so callers (Desktop
# session-id search) don't have to fetch every row and filter in
@ -6730,6 +6742,45 @@ class SessionDB:
s.pop("_effective_last_active", None)
sessions.append(s)
# Back-fill pinned conversations the page missed. A pin outlives
# recency, so this runs BEFORE compression projection below — a
# back-filled root then projects to its live tip exactly like a row
# that had made the page on its own. One extra query, bounded by the
# number of pins (a handful), never N+1 per pin.
if include_pinned:
seen_ids = {s["id"] for s in sessions}
pinned_where = (
f"{where_sql} AND s.pinned = 1" if where_sql else "WHERE s.pinned = 1"
)
_sel = self._compact_session_cols() if compact_rows else "s.*"
pinned_query = f"""
SELECT {_sel},
COALESCE(
(SELECT {_PREVIEW_RAW_SELECT}
FROM messages m
WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
ORDER BY m.timestamp, m.id LIMIT 1),
''
) AS _preview_raw,
COALESCE(
(SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
s.started_at
) AS last_active
FROM sessions s
{pinned_where}
ORDER BY s.started_at DESC
"""
with self._read_ctx() as conn:
pinned_cursor = conn.execute(pinned_query, base_where_params)
pinned_rows = pinned_cursor.fetchall()
for row in pinned_rows:
s = dict(row)
if s["id"] in seen_ids:
continue
s["preview"] = _shape_preview(s.pop("_preview_raw", ""))
seen_ids.add(s["id"])
sessions.append(s)
# Project compression roots forward to their tips. Each row whose
# end_reason is 'compression' has a continuation child; replace the
# surfaced fields (id, message_count, title, last_active, ended_at,

View file

@ -6960,6 +6960,99 @@ class TestSessionPinAndStaleArchive:
assert self._pinned(db, "root") == 1
assert self._pinned(db, "tip") == 1
# ── pinned back-fill past the page window ─────────────────────────────
def test_pinned_session_survives_the_limit_window(self, db):
"""A pin outlives recency: paging must not evict a pinned row.
Without ``include_pinned`` the desktop's Pinned section renders empty
for any conversation that has aged off the sidebar page.
"""
for i in range(6):
self._make_idle(db, f"s{i}", days_idle=6 - i)
db.set_session_pinned("s0", True) # the oldest — off a 3-row page
def ids(**kw):
return [
s["id"]
for s in db.list_sessions_rich(
limit=3, min_message_count=1, order_by_last_active=True, **kw
)
]
page = ids()
assert "s0" not in page, "precondition: the pin is off the page"
with_pins = ids(include_pinned=True)
assert "s0" in with_pins
# The page itself is untouched; the pin is additive.
assert with_pins[:3] == page
assert len(with_pins) == len(page) + 1
def test_pinned_backfill_still_obeys_the_page_filters(self, db):
"""A back-filled pin is not a bypass — archived/short rows stay out."""
for i in range(4):
self._make_idle(db, f"f{i}", days_idle=4 - i)
self._make_idle(db, "archived_pin", days_idle=9)
db.set_session_pinned("archived_pin", True)
db.set_session_archived("archived_pin", True)
# Pinned but with no messages at all.
db.create_session(session_id="empty_pin", source="cli")
db.set_session_pinned("empty_pin", True)
ids = [
s["id"]
for s in db.list_sessions_rich(
limit=2,
min_message_count=1,
order_by_last_active=True,
include_pinned=True,
)
]
assert "archived_pin" not in ids
assert "empty_pin" not in ids
def test_pinned_backfill_does_not_duplicate_an_on_page_row(self, db):
for i in range(3):
self._make_idle(db, f"p{i}", days_idle=3 - i)
db.set_session_pinned("p2", True) # newest — already on the page
ids = [
s["id"]
for s in db.list_sessions_rich(
limit=3,
min_message_count=1,
order_by_last_active=True,
include_pinned=True,
)
]
assert ids.count("p2") == 1
def test_pinned_backfill_projects_a_compression_root_to_its_tip(self, db):
"""A back-filled root goes through tip projection like any other row."""
for i in range(4):
self._make_idle(db, f"n{i}", days_idle=4 - i)
self._make_idle(db, "old_root", days_idle=30)
db.end_session("old_root", end_reason="compression")
db.create_session(
session_id="old_tip", source="cli", parent_session_id="old_root"
)
db.append_message(session_id="old_tip", role="user", content="continued")
db.set_session_pinned("old_root", True)
rows = db.list_sessions_rich(
limit=2,
min_message_count=1,
order_by_last_active=True,
include_pinned=True,
)
backfilled = next(s for s in rows if s.get("_lineage_root_id") == "old_root")
# Surfaced under the live tip's identity, keyed on the durable root.
assert backfilled["id"] == "old_tip"
# ── stale archive ─────────────────────────────────────────────────────
def test_archives_only_sessions_idle_past_threshold(self, db):
self._make_idle(db, "stale", days_idle=5)