fix: bound sidebar session-list cache
This commit is contained in:
1 parent
b1a5ade137
commit
7150b8fb2b
4 files changed
+201
-11
No files matched your search
+30
-5
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user