fix: exclude hidden snapshots from pin quota
This commit is contained in:
1 parent
7dc4273a21
commit
09d2f4be2f
6 files changed
+70
-4
No files matched your search
@@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hidden pre-compression snapshots no longer keep stale pin state or count toward the visible pinned-session quota (#3181).
|
||||||
|
|
||||||
## [v0.51.171] — 2026-05-30 — Release EQ (stage-batch53 — tool-output card badge + Neon opt-in skin)
|
## [v0.51.171] — 2026-05-30 — Release EQ (stage-batch53 — tool-output card badge + Neon opt-in skin)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+22
-2
@@ -99,6 +99,25 @@ def _session_field(session, field, default=None):
|
|||||||
return getattr(session, field, default)
|
return getattr(session, field, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _session_counts_toward_pin_quota(session) -> bool:
|
||||||
|
"""Return True when a pinned session should consume visible pin quota."""
|
||||||
|
if not _session_field(session, "pinned", False):
|
||||||
|
return False
|
||||||
|
if _session_field(session, "archived", False):
|
||||||
|
return False
|
||||||
|
if isinstance(session, dict):
|
||||||
|
row = session
|
||||||
|
elif hasattr(session, "compact"):
|
||||||
|
row = session.compact()
|
||||||
|
else:
|
||||||
|
row = {
|
||||||
|
"pre_compression_snapshot": _session_field(session, "pre_compression_snapshot", False),
|
||||||
|
"source_tag": _session_field(session, "source_tag", None),
|
||||||
|
"default_hidden": _session_field(session, "default_hidden", False),
|
||||||
|
}
|
||||||
|
return not _hide_from_default_sidebar(row)
|
||||||
|
|
||||||
|
|
||||||
# ── Profile-scoped session/project filtering (#1611, #1614) ────────────────
|
# ── Profile-scoped session/project filtering (#1611, #1614) ────────────────
|
||||||
#
|
#
|
||||||
# Sessions and projects are stored in the WebUI sidecar without per-row
|
# Sessions and projects are stored in the WebUI sidecar without per-row
|
||||||
@@ -2730,6 +2749,7 @@ from api.models import (
|
|||||||
merge_session_messages_append_only,
|
merge_session_messages_append_only,
|
||||||
_session_message_merge_key,
|
_session_message_merge_key,
|
||||||
_is_empty_partial_activity_message,
|
_is_empty_partial_activity_message,
|
||||||
|
_hide_from_default_sidebar,
|
||||||
prune_session_from_index,
|
prune_session_from_index,
|
||||||
ensure_cron_project,
|
ensure_cron_project,
|
||||||
is_cron_session,
|
is_cron_session,
|
||||||
@@ -6509,7 +6529,7 @@ def handle_post(handler, parsed) -> bool:
|
|||||||
# so must run outside our own LOCK acquire below).
|
# so must run outside our own LOCK acquire below).
|
||||||
persisted_pinned_ids = {
|
persisted_pinned_ids = {
|
||||||
_session_field(existing, "session_id", None) for existing in all_sessions()
|
_session_field(existing, "session_id", None) for existing in all_sessions()
|
||||||
if _session_field(existing, "pinned", False) and not _session_field(existing, "archived", False)
|
if _session_counts_toward_pin_quota(existing)
|
||||||
}
|
}
|
||||||
with LOCK:
|
with LOCK:
|
||||||
# Final authoritative count: merge persisted-pinned with the
|
# Final authoritative count: merge persisted-pinned with the
|
||||||
@@ -6519,7 +6539,7 @@ def handle_post(handler, parsed) -> bool:
|
|||||||
pinned_ids = set(persisted_pinned_ids)
|
pinned_ids = set(persisted_pinned_ids)
|
||||||
pinned_ids.update(
|
pinned_ids.update(
|
||||||
sid for sid, existing in SESSIONS.items()
|
sid for sid, existing in SESSIONS.items()
|
||||||
if getattr(existing, "pinned", False) and not getattr(existing, "archived", False)
|
if _session_counts_toward_pin_quota(existing)
|
||||||
)
|
)
|
||||||
pinned_ids.discard(body["session_id"])
|
pinned_ids.discard(body["session_id"])
|
||||||
pinned_sessions_limit = int(load_settings().get("pinned_sessions_limit", 3) or 3)
|
pinned_sessions_limit = int(load_settings().get("pinned_sessions_limit", 3) or 3)
|
||||||
|
|||||||
@@ -2286,8 +2286,10 @@ def _preserve_pre_compression_snapshot(s, old_sid: str) -> None:
|
|||||||
# pre-existing parent_session_id lineage.
|
# pre-existing parent_session_id lineage.
|
||||||
saved_sid = s.session_id
|
saved_sid = s.session_id
|
||||||
saved_snapshot = bool(getattr(s, 'pre_compression_snapshot', False))
|
saved_snapshot = bool(getattr(s, 'pre_compression_snapshot', False))
|
||||||
|
saved_pinned = bool(getattr(s, 'pinned', False))
|
||||||
s.session_id = old_sid
|
s.session_id = old_sid
|
||||||
s.pre_compression_snapshot = True
|
s.pre_compression_snapshot = True
|
||||||
|
s.pinned = False
|
||||||
# Stage-359 / PR #2295: clear runtime stream-state fields on the
|
# Stage-359 / PR #2295: clear runtime stream-state fields on the
|
||||||
# archived snapshot so the sidebar does not reopen the parent as
|
# archived snapshot so the sidebar does not reopen the parent as
|
||||||
# a permanently-running session while the child already holds the
|
# a permanently-running session while the child already holds the
|
||||||
@@ -2314,6 +2316,7 @@ def _preserve_pre_compression_snapshot(s, old_sid: str) -> None:
|
|||||||
finally:
|
finally:
|
||||||
s.session_id = saved_sid
|
s.session_id = saved_sid
|
||||||
s.pre_compression_snapshot = saved_snapshot
|
s.pre_compression_snapshot = saved_snapshot
|
||||||
|
s.pinned = saved_pinned
|
||||||
s.active_stream_id = saved_active_stream_id
|
s.active_stream_id = saved_active_stream_id
|
||||||
s.pending_user_message = saved_pending_user_message
|
s.pending_user_message = saved_pending_user_message
|
||||||
s.pending_attachments = saved_pending_attachments
|
s.pending_attachments = saved_pending_attachments
|
||||||
@@ -2326,6 +2329,7 @@ def _preserve_pre_compression_snapshot(s, old_sid: str) -> None:
|
|||||||
snapshot = Session.load(old_sid)
|
snapshot = Session.load(old_sid)
|
||||||
if snapshot:
|
if snapshot:
|
||||||
snapshot.pre_compression_snapshot = True
|
snapshot.pre_compression_snapshot = True
|
||||||
|
snapshot.pinned = False
|
||||||
# Stage-359 Opus SHOULD-FIX: clear runtime fields on the loaded
|
# Stage-359 Opus SHOULD-FIX: clear runtime fields on the loaded
|
||||||
# snapshot too. If the disk snapshot was last persisted while the
|
# snapshot too. If the disk snapshot was last persisted while the
|
||||||
# parent was live, it could carry a stale active_stream_id /
|
# parent was live, it could carry a stale active_stream_id /
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class FakeSession:
|
|||||||
self.session_id = "new_session"
|
self.session_id = "new_session"
|
||||||
self.parent_session_id = "original_parent"
|
self.parent_session_id = "original_parent"
|
||||||
self.pre_compression_snapshot = False
|
self.pre_compression_snapshot = False
|
||||||
|
self.pinned = True
|
||||||
self.active_stream_id = "live-stream"
|
self.active_stream_id = "live-stream"
|
||||||
self.pending_user_message = "current prompt"
|
self.pending_user_message = "current prompt"
|
||||||
self.pending_attachments = [{"name": "file.txt"}]
|
self.pending_attachments = [{"name": "file.txt"}]
|
||||||
@@ -21,6 +22,7 @@ class FakeSession:
|
|||||||
"session_id": self.session_id,
|
"session_id": self.session_id,
|
||||||
"parent_session_id": self.parent_session_id,
|
"parent_session_id": self.parent_session_id,
|
||||||
"pre_compression_snapshot": self.pre_compression_snapshot,
|
"pre_compression_snapshot": self.pre_compression_snapshot,
|
||||||
|
"pinned": self.pinned,
|
||||||
"active_stream_id": self.active_stream_id,
|
"active_stream_id": self.active_stream_id,
|
||||||
"pending_user_message": self.pending_user_message,
|
"pending_user_message": self.pending_user_message,
|
||||||
"pending_attachments": list(self.pending_attachments),
|
"pending_attachments": list(self.pending_attachments),
|
||||||
@@ -43,6 +45,7 @@ def test_preserve_pre_compression_snapshot_clears_runtime_fields_while_restoring
|
|||||||
"session_id": "old_session",
|
"session_id": "old_session",
|
||||||
"parent_session_id": "original_parent",
|
"parent_session_id": "original_parent",
|
||||||
"pre_compression_snapshot": True,
|
"pre_compression_snapshot": True,
|
||||||
|
"pinned": False,
|
||||||
"active_stream_id": None,
|
"active_stream_id": None,
|
||||||
"pending_user_message": None,
|
"pending_user_message": None,
|
||||||
"pending_attachments": [],
|
"pending_attachments": [],
|
||||||
@@ -52,6 +55,7 @@ def test_preserve_pre_compression_snapshot_clears_runtime_fields_while_restoring
|
|||||||
}
|
}
|
||||||
assert session.session_id == "new_session"
|
assert session.session_id == "new_session"
|
||||||
assert session.pre_compression_snapshot is False
|
assert session.pre_compression_snapshot is False
|
||||||
|
assert session.pinned is True
|
||||||
assert session.active_stream_id == "live-stream"
|
assert session.active_stream_id == "live-stream"
|
||||||
assert session.pending_user_message == "current prompt"
|
assert session.pending_user_message == "current prompt"
|
||||||
assert session.pending_attachments == [{"name": "file.txt"}]
|
assert session.pending_attachments == [{"name": "file.txt"}]
|
||||||
@@ -59,6 +63,7 @@ def test_preserve_pre_compression_snapshot_clears_runtime_fields_while_restoring
|
|||||||
|
|
||||||
saved = json.loads((tmp_path / "old_session.json").read_text(encoding="utf-8"))
|
saved = json.loads((tmp_path / "old_session.json").read_text(encoding="utf-8"))
|
||||||
assert saved["pre_compression_snapshot"] is True
|
assert saved["pre_compression_snapshot"] is True
|
||||||
|
assert saved["pinned"] is False
|
||||||
assert saved["active_stream_id"] is None
|
assert saved["active_stream_id"] is None
|
||||||
assert saved["pending_user_message"] is None
|
assert saved["pending_user_message"] is None
|
||||||
assert saved["pending_attachments"] == []
|
assert saved["pending_attachments"] == []
|
||||||
@@ -77,6 +82,7 @@ def test_preserve_pre_compression_snapshot_load_and_mark_branch_clears_runtime_f
|
|||||||
{"role": "user", "content": "newer prompt already on disk"},
|
{"role": "user", "content": "newer prompt already on disk"},
|
||||||
],
|
],
|
||||||
"pre_compression_snapshot": True,
|
"pre_compression_snapshot": True,
|
||||||
|
"pinned": True,
|
||||||
"active_stream_id": "stale-stream",
|
"active_stream_id": "stale-stream",
|
||||||
"pending_user_message": "stale prompt",
|
"pending_user_message": "stale prompt",
|
||||||
"pending_attachments": [{"name": "stale.txt"}],
|
"pending_attachments": [{"name": "stale.txt"}],
|
||||||
@@ -91,6 +97,7 @@ def test_preserve_pre_compression_snapshot_load_and_mark_branch_clears_runtime_f
|
|||||||
saved = json.loads((tmp_path / "old_session.json").read_text(encoding="utf-8"))
|
saved = json.loads((tmp_path / "old_session.json").read_text(encoding="utf-8"))
|
||||||
assert saved["messages"] == old_payload["messages"]
|
assert saved["messages"] == old_payload["messages"]
|
||||||
assert saved["pre_compression_snapshot"] is True
|
assert saved["pre_compression_snapshot"] is True
|
||||||
|
assert saved["pinned"] is False
|
||||||
assert saved["active_stream_id"] is None
|
assert saved["active_stream_id"] is None
|
||||||
assert saved["pending_user_message"] is None
|
assert saved["pending_user_message"] is None
|
||||||
assert saved["pending_attachments"] == []
|
assert saved["pending_attachments"] == []
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from types import SimpleNamespace
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
@@ -67,6 +68,35 @@ def test_session_pin_endpoint_caps_pinned_sessions_at_three():
|
|||||||
post("/api/session/delete", {"session_id": sid})
|
post("/api/session/delete", {"session_id": sid})
|
||||||
|
|
||||||
|
|
||||||
|
def test_hidden_pre_compression_snapshot_does_not_count_toward_pin_quota():
|
||||||
|
from api.routes import _session_counts_toward_pin_quota
|
||||||
|
|
||||||
|
assert _session_counts_toward_pin_quota({
|
||||||
|
"session_id": "hidden-snapshot",
|
||||||
|
"pinned": True,
|
||||||
|
"archived": False,
|
||||||
|
"pre_compression_snapshot": True,
|
||||||
|
}) is False
|
||||||
|
assert _session_counts_toward_pin_quota({
|
||||||
|
"session_id": "visible-session",
|
||||||
|
"pinned": True,
|
||||||
|
"archived": False,
|
||||||
|
"pre_compression_snapshot": False,
|
||||||
|
}) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_hidden_in_memory_snapshot_does_not_count_toward_pin_quota():
|
||||||
|
from api.routes import _session_counts_toward_pin_quota
|
||||||
|
|
||||||
|
snapshot = SimpleNamespace(
|
||||||
|
session_id="hidden-memory-snapshot",
|
||||||
|
pinned=True,
|
||||||
|
archived=False,
|
||||||
|
pre_compression_snapshot=True,
|
||||||
|
)
|
||||||
|
assert _session_counts_toward_pin_quota(snapshot) is False
|
||||||
|
|
||||||
|
|
||||||
def test_session_pin_cap_has_backend_and_frontend_guards():
|
def test_session_pin_cap_has_backend_and_frontend_guards():
|
||||||
assert 'pinned_ids = {' in ROUTES_PY
|
assert 'pinned_ids = {' in ROUTES_PY
|
||||||
assert 'pinned_ids.update(' in ROUTES_PY
|
assert 'pinned_ids.update(' in ROUTES_PY
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ def test_session_field_helper_reads_dicts_and_objects():
|
|||||||
|
|
||||||
|
|
||||||
def test_pin_limit_snapshot_counts_index_dict_entries():
|
def test_pin_limit_snapshot_counts_index_dict_entries():
|
||||||
|
assert "def _session_counts_toward_pin_quota(session)" in ROUTES_PY
|
||||||
assert "_session_field(existing, \"session_id\", None)" in ROUTES_PY
|
assert "_session_field(existing, \"session_id\", None)" in ROUTES_PY
|
||||||
assert "_session_field(existing, \"pinned\", False)" in ROUTES_PY
|
assert "_session_counts_toward_pin_quota(existing)" in ROUTES_PY
|
||||||
assert "_session_field(existing, \"archived\", False)" in ROUTES_PY
|
assert "_hide_from_default_sidebar(row)" in ROUTES_PY
|
||||||
start = ROUTES_PY.find("persisted_pinned_ids = {")
|
start = ROUTES_PY.find("persisted_pinned_ids = {")
|
||||||
assert start != -1, "persisted pin snapshot not found"
|
assert start != -1, "persisted pin snapshot not found"
|
||||||
end = ROUTES_PY.find("with LOCK:", start)
|
end = ROUTES_PY.find("with LOCK:", start)
|
||||||
|
|||||||
Reference in new issue
Block a user