Architecture: How to persist activeSessionId across page refreshes without...

Documentation for Sessions

🏗️ Architecture: How to persist activeSessionId across page refreshes without...

Date: 2026-03-03
Time: 22:30:02 UTC
Session: gspace1-166.13
Agent: storage
Confidence: 97%
Status: ✅ Documented


📋 Decision Context

Question:

How to persist activeSessionId across page refreshes without it being lost or overwritten?

Task: Session 170: Fix activeSessionId lost on page refresh


🎯 Decision Made

Chosen:

Extracted activeSessionId out of autoFlashState into a dedicated signal() + localStorage. The signal is initialized synchronously from localStorage.getItem() in the field declaration. A separate setActiveSessionId() method writes to both signal and localStorage. ALL code paths that read or write activeSessionId were migrated from state().activeSessionId to _activeSessionId() / setActiveSessionId(). autoFlashState still stores session data (messages, titles) but no longer controls which session is active.

Options Considered:

  1. Keep activeSessionId in autoFlashState (broken — async hydration overwrites)
  2. Skip first effect run (failed — multiple signal changes re-trigger)
  3. localStorage read + state.update in constructor (failed — autoFlashState hydration overwrites the update)
  4. Separate signal + localStorage (CHOSEN — synchronous, deterministic, immune to hydration)

💭 Reasoning

ROOT CAUSE: activeSessionId was stored INSIDE autoFlashState alongside all session data. autoFlashState hydration is async (setInterval 50ms polling). On page refresh: (1) autoFlashState returns DEFAULT_STATE with activeSessionId:null, (2) constructor effect fires and sees null → creates/selects wrong session, (3) 50ms later IndexedDB hydrates but the damage is done. MULTIPLE FAILED ATTEMPTS: (a) Skip-first-effect-run — failed because effect re-fires from multiple signal changes. (b) localStorage read + state.update in constructor — failed because state.update triggers async dispatch which autoFlashState hydration then overwrites. (c) waitingForBackend flag — failed because setBackendSessions() also overwrote activeSessionId. FINAL FIX: Completely decouple activeSessionId from autoFlashState. A plain signal() initialized from localStorage is synchronous, deterministic, and immune to IndexedDB hydration timing. This matches the industry pattern (Slack, Discord use URL/localStorage for active conversation, not IndexedDB). Also fixed setBackendSessions() which had a secondary bug: it checked state().sessions[activeId] (local only) instead of mergedSessions()[activeId], always found null, and overrode the active session.


📁 Files Involved


🔗 Related


Auto-generated by MCP Smart Consolidation