Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tests/test_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,30 @@ def test_session_list_includes_folder_id(server_url):
assert s["folder_id"] is None


def test_reap_stale_chat_sessions_passes_user_id(server_url):
"""Regression for the daemon-thread crash where the reaper called
remove_chat_session(sid) without user_id and the call raised
TypeError on every cycle. Pre-fix this caused stale sessions to
accumulate forever (the reaper thread died on first invocation)."""
with _client(server_url) as c:
_register(c, "alice")
sid = c.post("/api/prompt",
json={"prompt": "", "session_id": ""}).json()["session_id"]
# Force this session into the "stale + idle" state by rewinding
# its last_active far enough that is_stale() returns True.
from web import api as _apimod
sess = _apimod._chat_sessions.get(sid)
assert sess is not None, "session should be in the in-memory cache"
import time as _time
sess.last_active = _time.monotonic() - 1e9 # very old
# Reaper must run cleanly — pre-fix this raised
# TypeError: remove_chat_session() missing 1 required positional
# argument: 'user_id'
_apimod.reap_stale_chat_sessions()
# And it must actually evict the session
assert sid not in _apimod._chat_sessions


def test_cross_user_isolation(server_url):
with _client(server_url) as ca:
_register(ca, "alice")
Expand Down
16 changes: 11 additions & 5 deletions web/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,11 +1133,17 @@ def get_available_models() -> list[dict]:


def reap_stale_chat_sessions():
"""Called periodically by server.py's reaper thread."""
stale: list[str] = []
"""Called periodically by server.py's reaper thread.

`remove_chat_session` requires the owning user_id for ownership-check
parity with the per-user DELETE endpoint, so we capture it from the
cached ChatSession object — collecting `(sid, user_id)` pairs under the
lock and applying outside it (remove_chat_session re-acquires).
"""
stale: list[tuple[str, int]] = []
with _chat_lock:
for sid, session in _chat_sessions.items():
if session.is_stale() and session.is_idle():
stale.append(sid)
for sid in stale:
remove_chat_session(sid)
stale.append((sid, session.user_id))
for sid, user_id in stale:
remove_chat_session(sid, user_id)
Loading