From f75e17c9121ce70e1604c9b7214fd7bab7d8c242 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Fri, 3 Apr 2026 19:50:08 +0000 Subject: [PATCH] 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. --- api/models.py | 11 ++++++++++- static/sessions.js | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/api/models.py b/api/models.py index ad9d807..be6b68e 100644 --- a/api/models.py +++ b/api/models.py @@ -90,6 +90,11 @@ def all_sessions(): 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.) 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 except Exception: pass # fall through to full scan @@ -105,7 +110,11 @@ def all_sessions(): for s in SESSIONS.values(): 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) - 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'): diff --git a/static/sessions.js b/static/sessions.js index dba98d5..976c54e 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -115,7 +115,9 @@ function renderSessionListFromCache(){ const titleIds=new Set(titleMatches.map(s=>s.session_id)); const allMatched=q?[...titleMatches,..._contentSearchResults.filter(s=>!titleIds.has(s.session_id))]:titleMatches; // 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 const projectFiltered=_activeProject?profileFiltered.filter(s=>s.project_id===_activeProject):profileFiltered; // Filter archived unless toggle is on