fix: legacy sessions (profile=null) leak into all profiles' session lists

Root cause: sessions created before Sprint 22 have no profile tag (profile=None).
The client filter was '!s.profile || s.profile === S.activeProfile' -- the
'!s.profile' guard made ALL 33 legacy sessions visible under every profile,
so switching to Camanji still showed the entire default session history.

Fix:
- api/models.py all_sessions(): backfill profile='default' on sessions with
  no profile tag before returning. This is in-memory only (no disk writes) --
  legacy sessions just get attributed to the default profile at read time.
  Applied to both the index-path and the full-scan fallback path.
- static/sessions.js: tighten the client filter to s.profile === S.activeProfile
  (remove the '!s.profile' escape hatch -- now redundant since server fills it).
  Every session now has an explicit profile, so the filter is precise.

Result: switching to Camanji shows only Camanji sessions. Default profile shows
legacy + default-tagged sessions. 'All profiles' toggle still shows everything.
S.activeProfile defaults to 'default' in the S object so first render is safe.

Tests: 426 passed, 0 failed.
This commit is contained in:
Nathan Esquenazi
2026-04-03 19:50:08 +00:00
parent 3d8cf85ef2
commit f75e17c912
2 changed files with 13 additions and 2 deletions

View File

@@ -90,6 +90,11 @@ def all_sessions():
result = sorted(index_map.values(), key=lambda s: (s.get('pinned', False), s['updated_at']), reverse=True) result = sorted(index_map.values(), key=lambda s: (s.get('pinned', False), s['updated_at']), reverse=True)
# Hide empty Untitled sessions from the UI (created by tests, page refreshes, etc.) # Hide empty Untitled sessions from the UI (created by tests, page refreshes, etc.)
result = [s for s in result if not (s.get('title','Untitled')=='Untitled' and s.get('message_count',0)==0)] result = [s for s in result if not (s.get('title','Untitled')=='Untitled' and s.get('message_count',0)==0)]
# Backfill: sessions created before Sprint 22 have no profile tag.
# Attribute them to 'default' so the client profile filter works correctly.
for s in result:
if not s.get('profile'):
s['profile'] = 'default'
return result return result
except Exception: except Exception:
pass # fall through to full scan pass # fall through to full scan
@@ -105,7 +110,11 @@ def all_sessions():
for s in SESSIONS.values(): for s in SESSIONS.values():
if all(s.session_id != x.session_id for x in out): out.append(s) if all(s.session_id != x.session_id for x in out): out.append(s)
out.sort(key=lambda s: (getattr(s, 'pinned', False), s.updated_at), reverse=True) out.sort(key=lambda s: (getattr(s, 'pinned', False), s.updated_at), reverse=True)
return [s.compact() for s in out if not (s.title=='Untitled' and len(s.messages)==0)] result = [s.compact() for s in out if not (s.title=='Untitled' and len(s.messages)==0)]
for s in result:
if not s.get('profile'):
s['profile'] = 'default'
return result
def title_from(messages, fallback='Untitled'): def title_from(messages, fallback='Untitled'):

View File

@@ -115,7 +115,9 @@ function renderSessionListFromCache(){
const titleIds=new Set(titleMatches.map(s=>s.session_id)); const titleIds=new Set(titleMatches.map(s=>s.session_id));
const allMatched=q?[...titleMatches,..._contentSearchResults.filter(s=>!titleIds.has(s.session_id))]:titleMatches; const allMatched=q?[...titleMatches,..._contentSearchResults.filter(s=>!titleIds.has(s.session_id))]:titleMatches;
// Filter by active profile (unless "All profiles" is toggled on) // Filter by active profile (unless "All profiles" is toggled on)
const profileFiltered=_showAllProfiles?allMatched:allMatched.filter(s=>!s.profile||s.profile===S.activeProfile); // Server backfills profile='default' for legacy sessions, so every session has a profile.
// Show only sessions tagged to the active profile; 'All profiles' toggle overrides.
const profileFiltered=_showAllProfiles?allMatched:allMatched.filter(s=>s.profile===S.activeProfile);
// Filter by active project // Filter by active project
const projectFiltered=_activeProject?profileFiltered.filter(s=>s.project_id===_activeProject):profileFiltered; const projectFiltered=_activeProject?profileFiltered.filter(s=>s.project_id===_activeProject):profileFiltered;
// Filter archived unless toggle is on // Filter archived unless toggle is on