288 lines
9.2 KiB
TypeScript
288 lines
9.2 KiB
TypeScript
/**
|
|
* 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<string>()
|
|
|
|
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<MigrationResult> {
|
|
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<MigrationResult> {
|
|
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()
|
|
}
|