Files
NomadArch/packages/ui/src/stores/session-migration.ts
Gemini AI eca090d360
Some checks failed
Release Binaries / release (push) Has been cancelled
Improve session migration: scan ALL localStorage caches to handle workspace ID changes
2025-12-27 11:35:40 +04:00

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()
}