/** * Session Migration - Handles importing sessions when switching between SDK and Native modes * * This module caches SDK session data to localStorage so it can be imported to Native mode * when the user switches modes. */ import { nativeSessionApi } from "../lib/lite-mode" import { sessions } from "./session-state" import { getLogger } from "../lib/logger" import type { Session } from "../types/session" const log = getLogger("session-migration") // LocalStorage key prefix for cached SDK sessions const SDK_SESSION_CACHE_PREFIX = "nomadarch_sdk_sessions_" // Track which workspaces have already been migrated to prevent duplicate migrations const migratedWorkspaces = new Set() export interface CachedSession { id: string title?: string parentId?: string | null createdAt?: number updatedAt?: number model?: { providerId: string; modelId: string } agent?: string } export interface MigrationResult { success: boolean imported: number skipped: number error?: string } /** * Cache SDK sessions to localStorage for later migration * This should be called whenever sessions are fetched in SDK mode */ export function cacheSDKSessions(workspaceId: string, sessionList: Session[]): void { if (sessionList.length === 0) return try { const cached: CachedSession[] = sessionList.map(s => ({ id: s.id, title: s.title, parentId: s.parentId, createdAt: s.time?.created, updatedAt: s.time?.updated, model: s.model, agent: s.agent })) const key = SDK_SESSION_CACHE_PREFIX + workspaceId localStorage.setItem(key, JSON.stringify(cached)) log.info({ workspaceId, count: cached.length }, "Cached SDK sessions for migration") } catch (error) { log.error({ workspaceId, error }, "Failed to cache SDK sessions") } } /** * Get cached SDK sessions from localStorage */ export function getCachedSDKSessions(workspaceId: string): CachedSession[] { try { const key = SDK_SESSION_CACHE_PREFIX + workspaceId const cached = localStorage.getItem(key) if (!cached) return [] const sessions = JSON.parse(cached) as CachedSession[] log.info({ workspaceId, count: sessions.length }, "Retrieved cached SDK sessions") return sessions } catch (error) { log.error({ workspaceId, error }, "Failed to retrieve cached SDK sessions") return [] } } /** * Get ALL cached sessions from localStorage across all workspace IDs * This is useful for migrating sessions when workspace IDs have changed */ export function getAllCachedSessions(): { workspaceId: string; sessions: CachedSession[] }[] { const results: { workspaceId: string; sessions: CachedSession[] }[] = [] try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) if (key && key.startsWith(SDK_SESSION_CACHE_PREFIX)) { const workspaceId = key.substring(SDK_SESSION_CACHE_PREFIX.length) const cached = localStorage.getItem(key) if (cached) { try { const sessions = JSON.parse(cached) as CachedSession[] if (sessions.length > 0) { results.push({ workspaceId, sessions }) log.info({ workspaceId, count: sessions.length }, "Found cached sessions") } } catch (e) { // Invalid JSON, skip } } } } } catch (error) { log.error({ error }, "Failed to scan localStorage for cached sessions") } return results } /** * Clear cached SDK sessions after successful migration */ export function clearCachedSDKSessions(workspaceId: string): void { try { const key = SDK_SESSION_CACHE_PREFIX + workspaceId localStorage.removeItem(key) log.info({ workspaceId }, "Cleared cached SDK sessions") } catch (error) { log.error({ workspaceId, error }, "Failed to clear cached SDK sessions") } } /** * Check if a workspace needs session migration */ export function needsMigration(workspaceId: string): boolean { return !migratedWorkspaces.has(workspaceId) } /** * Mark a workspace as migrated */ export function markMigrated(workspaceId: string): void { migratedWorkspaces.add(workspaceId) } /** * Get existing SDK sessions for a workspace from the local store */ export function getExistingSdkSessions(instanceId: string): Session[] { const instanceSessions = sessions().get(instanceId) if (!instanceSessions) return [] return Array.from(instanceSessions.values()) } /** * Migrate sessions from SDK mode to Native mode * This should be called when the user switches from an SDK binary to native mode */ export async function migrateSessionsToNative( workspaceId: string, sdkSessions: Array<{ id: string title?: string parentId?: string | null createdAt?: number updatedAt?: number time?: { created?: number; updated?: number } model?: { providerId: string; modelId: string } agent?: string messages?: Array<{ id: string role: "user" | "assistant" | "system" | "tool" content?: string timestamp?: number }> }> ): Promise { if (sdkSessions.length === 0) { log.info({ workspaceId }, "No sessions to migrate") markMigrated(workspaceId) return { success: true, imported: 0, skipped: 0 } } try { log.info({ workspaceId, count: sdkSessions.length }, "Starting session migration to native mode") // Transform to the format expected by the native API const sessionsToImport = sdkSessions.map(s => ({ id: s.id, title: s.title, parentId: s.parentId, createdAt: s.createdAt || s.time?.created, updatedAt: s.updatedAt || s.time?.updated, model: s.model, agent: s.agent, messages: s.messages?.map(m => ({ id: m.id, role: m.role, content: m.content, createdAt: m.timestamp })) })) const result = await nativeSessionApi.importSessions(workspaceId, sessionsToImport) log.info({ workspaceId, ...result }, "Session migration completed") markMigrated(workspaceId) // Clear the cache after successful migration if (result.success) { clearCachedSDKSessions(workspaceId) } return { success: result.success, imported: result.imported, skipped: result.skipped } } catch (error) { log.error({ workspaceId, error }, "Session migration failed") return { success: false, imported: 0, skipped: 0, error: error instanceof Error ? error.message : String(error) } } } /** * Auto-import cached SDK sessions to native mode * This is the main entry point for automatic migration on startup */ export async function autoImportCachedSessions(workspaceId: string): Promise { if (!needsMigration(workspaceId)) { return { success: true, imported: 0, skipped: 0 } } // First, try to get cached sessions for this specific workspace ID let cachedSessions = getCachedSDKSessions(workspaceId) // If no sessions found for this workspace, check ALL cached sessions // This handles the case where workspace IDs changed (e.g., after the deterministic ID fix) if (cachedSessions.length === 0) { const allCached = getAllCachedSessions() if (allCached.length > 0) { log.info({ allCached: allCached.map(c => ({ id: c.workspaceId, count: c.sessions.length })) }, "Found cached sessions from other workspace IDs, importing all") // Combine all cached sessions for (const cache of allCached) { cachedSessions = cachedSessions.concat(cache.sessions) } } } if (cachedSessions.length === 0) { // Also check in-memory sessions as a fallback const memorySessions = getExistingSdkSessions(workspaceId) if (memorySessions.length > 0) { const migrationData = memorySessions.map(s => ({ id: s.id, title: s.title, parentId: s.parentId, time: s.time, model: s.model, agent: s.agent })) return migrateSessionsToNative(workspaceId, migrationData) } markMigrated(workspaceId) return { success: true, imported: 0, skipped: 0 } } return migrateSessionsToNative(workspaceId, cachedSessions) } /** * Clear migration status (for testing or when user explicitly wants to re-migrate) */ export function clearMigrationStatus(workspaceId: string): void { migratedWorkspaces.delete(workspaceId) } /** * Clear all migration statuses */ export function clearAllMigrationStatuses(): void { migratedWorkspaces.clear() }