fix: bound sidebar session-list cache

This commit is contained in:
Martin Dell committed 2026-09-09 21:22:38 +00:00
1 parent b1a5ade137
commit 7150b8fb2b
4 files changed
+201 -11

No files matched your search

+30 -5
View File
@@ -1721,7 +1721,12 @@ class Session:
n += 1
return n
def compact(self, include_runtime=False, active_stream_ids=None) -> dict:
def compact(
self,
include_runtime=False,
active_stream_ids=None,
sidebar_metadata_only=False,
) -> dict:
active_stream_ids = active_stream_ids if active_stream_ids is not None else set()
has_pending_user_message = bool(self.pending_user_message)
message_count = (
@@ -1734,7 +1739,7 @@ class Session:
last_message_at = _last_message_timestamp(self.messages) or self.updated_at
if has_pending_user_message and self.pending_started_at:
last_message_at = self.pending_started_at
return {
compact = {
'session_id': self.session_id,
'title': self.title,
'workspace': self.workspace,
@@ -1809,6 +1814,15 @@ class Session:
self.active_stream_id, active_stream_ids
) if include_runtime else False,
}
if sidebar_metadata_only:
for key in (
'compression_anchor_summary', 'compression_anchor_details',
'context_engine_state', 'compression_recovery',
'gateway_routing_history', 'composer_draft',
'process_wakeup_pause', 'share_token',
):
compact.pop(key, None)
return compact
PROCESS_WAKEUP_PROVIDER_UNAVAILABLE_TYPES = frozenset({
@@ -6278,7 +6292,12 @@ def _diag_stage(diag, name: str) -> None:
pass
def all_sessions(diag=None, *, include_lineage_metadata: bool = True):
def all_sessions(
diag=None,
*,
include_lineage_metadata: bool = True,
sidebar_metadata_only: bool = False,
):
_diag_stage(diag, "all_sessions.active_streams")
active_stream_ids = _active_stream_ids()
# Phase C: try index first for O(1) read; fall back to full scan
@@ -6318,7 +6337,7 @@ def all_sessions(diag=None, *, include_lineage_metadata: bool = True):
_diag_stage(diag, "all_sessions.backfill_load")
full = Session.load(s.get('session_id'))
if full:
index[i] = full.compact()
index[i] = full.compact(sidebar_metadata_only=sidebar_metadata_only)
backfilled.append(full)
if backfilled:
try:
@@ -6340,6 +6359,7 @@ def all_sessions(diag=None, *, include_lineage_metadata: bool = True):
index_map[s.session_id] = s.compact(
include_runtime=True,
active_stream_ids=active_stream_ids,
sidebar_metadata_only=sidebar_metadata_only,
)
missing_persisted_ids = []
if persisted_ids is not None:
@@ -6372,6 +6392,7 @@ def all_sessions(diag=None, *, include_lineage_metadata: bool = True):
index_map[sidecar.session_id] = sidecar.compact(
include_runtime=True,
active_stream_ids=active_stream_ids,
sidebar_metadata_only=sidebar_metadata_only,
)
recovered_sidecars.append(sidecar)
if recovered_sidecars:
@@ -6459,7 +6480,11 @@ def all_sessions(diag=None, *, include_lineage_metadata: bool = True):
# Hide empty Untitled sessions from the UI entirely — kept consistent with the
# index-path filter above. No grace window: a 0-message Untitled session is
# never shown regardless of age (#1171). Same streaming exemption as above (#1327).
result = [s.compact(include_runtime=True, active_stream_ids=active_stream_ids) for s in out if not (
result = [s.compact(
include_runtime=True,
active_stream_ids=active_stream_ids,
sidebar_metadata_only=sidebar_metadata_only,
) for s in out if not (
s.title == 'Untitled'
and len(s.messages) == 0
and not s.active_stream_id
+79 -5
View File
@@ -1,7 +1,6 @@
"""Session-list cache helpers extracted from api.routes."""
import os
import copy
import re
import threading
import time
@@ -44,6 +43,80 @@ _SESSIONS_CACHE_ALL_PROFILES_INVALIDATION_VERSION = 0
_SESSIONS_CACHE_PROFILE_INVALIDATION_VERSION: dict[str, int] = {}
def _session_list_cache_sidebar_fields() -> set[str]:
"""Return the canonical bounded field set used by the list response."""
try:
import api.routes as _routes
fields = getattr(_routes, "_SIDEBAR_SESSION_RESPONSE_FIELDS", None)
if isinstance(fields, set):
return fields
except Exception:
pass
# The fallback is intentionally small and contains no transcript-bearing
# fields. Normal runtime calls resolve the canonical set above after
# api.routes has finished importing.
return {
"session_id", "title", "workspace", "model", "model_provider",
"message_count", "user_message_count", "created_at", "updated_at",
"last_message_at", "pinned", "archived", "project_id", "profile",
"source_tag", "session_source", "is_streaming", "active_stream_id",
"has_pending_user_message", "pending_started_at", "default_hidden",
"worktree_path", "worktree_branch", "parent_session_id",
"parent_title", "parent_source", "relationship_type",
"pre_compression_snapshot", "read_only", "is_read_only",
"gateway_routing",
}
def _session_list_cache_bounded_payload(payload: dict) -> dict:
"""Project a builder payload to data the sidebar can actually consume.
Builders operate on full session snapshots because they also perform
reconciliation and lineage decisions. The cache must not retain those
snapshots: a single long transcript can otherwise make every cache set/get
allocate hundreds of megabytes via deepcopy().
"""
fields = _session_list_cache_sidebar_fields()
def project_rows(rows):
projected = []
for row in rows or []:
if not isinstance(row, dict):
projected.append({})
continue
item = {key: row[key] for key in fields if key in row}
if isinstance(item.get("gateway_routing"), dict):
item["gateway_routing"] = dict(item["gateway_routing"])
projected.append(item)
return projected
bounded = {
key: value
for key, value in payload.items()
if key not in {"sessions", "sidebar_reference_sessions"}
}
bounded["sessions"] = project_rows(payload.get("sessions"))
bounded["sidebar_reference_sessions"] = project_rows(
payload.get("sidebar_reference_sessions")
)
return bounded
def _session_list_cache_copy_payload(payload: dict) -> dict:
"""Copy only the already-bounded cache shape for request-local mutation."""
copied = dict(payload)
copied["sessions"] = [dict(row) for row in payload.get("sessions", [])]
copied["sidebar_reference_sessions"] = [
dict(row) for row in payload.get("sidebar_reference_sessions", [])
]
for rows in (copied["sessions"], copied["sidebar_reference_sessions"]):
for row in rows:
if isinstance(row.get("gateway_routing"), dict):
row["gateway_routing"] = dict(row["gateway_routing"])
return copied
def get_session_list_cache_snapshot() -> dict[str, object]:
"""Return scalar cache occupancy without waiting or changing LRU state.
@@ -240,7 +313,7 @@ def _session_list_cache_get(
if stamp != current_stamp:
if allow_stale:
_SESSIONS_CACHE.move_to_end(key)
return copy.deepcopy(payload), False
return _session_list_cache_copy_payload(payload), False
_SESSIONS_CACHE.pop(key, None)
return None, False
# #4808: widen the freshness window while a turn is streaming so the fixed
@@ -251,10 +324,10 @@ def _session_list_cache_get(
fresh = (now - ts) < ttl
if fresh:
_SESSIONS_CACHE.move_to_end(key)
return copy.deepcopy(payload), True
return _session_list_cache_copy_payload(payload), True
if allow_stale:
_SESSIONS_CACHE.move_to_end(key)
return copy.deepcopy(payload), False
return _session_list_cache_copy_payload(payload), False
_SESSIONS_CACHE.pop(key, None)
return None, False
@@ -282,8 +355,9 @@ def _session_list_cache_set(key: tuple, payload: dict) -> None:
if not isinstance(payload, dict):
return
stamp = _session_list_cache_resolved_source_stamp(key)
bounded = _session_list_cache_bounded_payload(payload)
with _SESSIONS_CACHE_LOCK:
_SESSIONS_CACHE[key] = (time.monotonic(), stamp, copy.deepcopy(payload))
_SESSIONS_CACHE[key] = (time.monotonic(), stamp, bounded)
_SESSIONS_CACHE.move_to_end(key)
while len(_SESSIONS_CACHE) > _SESSIONS_CACHE_MAX_ENTRIES:
_SESSIONS_CACHE.popitem(last=False)
+4 -1
View File
@@ -2295,8 +2295,11 @@ def _build_session_list_cache_payload(
)
def _all_sessions_for_sidebar():
kwargs = {"diag": diag, "include_lineage_metadata": False}
if _callable_accepts_kwarg(all_sessions, "sidebar_metadata_only"):
kwargs["sidebar_metadata_only"] = True
if _callable_accepts_kwarg(all_sessions, "include_lineage_metadata"):
return all_sessions(diag=diag, include_lineage_metadata=False)
return all_sessions(**kwargs)
# Focused tests and third-party callers sometimes monkeypatch
# routes.all_sessions with the historical diag-only signature.
return all_sessions(diag=diag)