Add automatic session migration when switching from SDK to Native mode
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
This commit is contained in:
33
packages/electron-app/.codenomad-data/mjnyjm5r/sessions.json
Normal file
33
packages/electron-app/.codenomad-data/mjnyjm5r/sessions.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"sessions": {
|
||||
"01KDFA3KMG1VSQNA217HZ3JAYA": {
|
||||
"id": "01KDFA3KMG1VSQNA217HZ3JAYA",
|
||||
"workspaceId": "mjnyjm5r",
|
||||
"title": "New Session",
|
||||
"parentId": null,
|
||||
"createdAt": 1766819221136,
|
||||
"updatedAt": 1766819221136,
|
||||
"messageIds": [],
|
||||
"model": {
|
||||
"providerId": "opencode-zen",
|
||||
"modelId": "grok-code"
|
||||
},
|
||||
"agent": "Assistant"
|
||||
},
|
||||
"01KDFA3SP5YJAB7B8BC2EM48NY": {
|
||||
"id": "01KDFA3SP5YJAB7B8BC2EM48NY",
|
||||
"workspaceId": "mjnyjm5r",
|
||||
"title": "New Session",
|
||||
"parentId": null,
|
||||
"createdAt": 1766819227333,
|
||||
"updatedAt": 1766819227333,
|
||||
"messageIds": [],
|
||||
"model": {
|
||||
"providerId": "opencode-zen",
|
||||
"modelId": "grok-code"
|
||||
},
|
||||
"agent": "Assistant"
|
||||
}
|
||||
},
|
||||
"messages": {}
|
||||
}
|
||||
@@ -158,6 +158,42 @@ export function registerNativeSessionsRoutes(app: FastifyInstance, deps: NativeS
|
||||
}
|
||||
})
|
||||
|
||||
// Import sessions from SDK mode - for migration when switching to native mode
|
||||
app.post<{
|
||||
Params: { workspaceId: string }
|
||||
Body: {
|
||||
sessions: Array<{
|
||||
id: string
|
||||
title?: string
|
||||
parentId?: string | null
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
model?: { providerId: string; modelId: string }
|
||||
agent?: string
|
||||
messages?: Array<{
|
||||
id: string
|
||||
role: "user" | "assistant" | "system" | "tool"
|
||||
content?: string
|
||||
createdAt?: number
|
||||
}>
|
||||
}>
|
||||
}
|
||||
}>("/api/native/workspaces/:workspaceId/sessions/import", async (request, reply) => {
|
||||
try {
|
||||
const result = await sessionManager.importSessions(
|
||||
request.params.workspaceId,
|
||||
request.body.sessions
|
||||
)
|
||||
logger.info({ workspaceId: request.params.workspaceId, ...result }, "Sessions imported from SDK mode")
|
||||
return { success: true, ...result }
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Failed to import sessions")
|
||||
reply.code(500)
|
||||
return { error: "Failed to import sessions" }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Get messages for a session
|
||||
app.get<{ Params: { workspaceId: string; sessionId: string } }>("/api/native/workspaces/:workspaceId/sessions/:sessionId/messages", async (request, reply) => {
|
||||
try {
|
||||
|
||||
@@ -311,6 +311,74 @@ export class NativeSessionManager {
|
||||
const store = this.stores.get(workspaceId)
|
||||
return store ? Object.keys(store.sessions).length : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Import sessions from SDK mode format - for migration when switching modes
|
||||
*/
|
||||
async importSessions(workspaceId: string, sessions: Array<{
|
||||
id: string
|
||||
title?: string
|
||||
parentId?: string | null
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
model?: { providerId: string; modelId: string }
|
||||
agent?: string
|
||||
messages?: Array<{
|
||||
id: string
|
||||
role: "user" | "assistant" | "system" | "tool"
|
||||
content?: string
|
||||
createdAt?: number
|
||||
}>
|
||||
}>): Promise<{ imported: number; skipped: number }> {
|
||||
const store = await this.loadStore(workspaceId)
|
||||
let imported = 0
|
||||
let skipped = 0
|
||||
|
||||
for (const sdkSession of sessions) {
|
||||
// Skip if session already exists
|
||||
if (store.sessions[sdkSession.id]) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const session: Session = {
|
||||
id: sdkSession.id,
|
||||
workspaceId,
|
||||
title: sdkSession.title || "Imported Session",
|
||||
parentId: sdkSession.parentId ?? null,
|
||||
createdAt: sdkSession.createdAt || now,
|
||||
updatedAt: sdkSession.updatedAt || now,
|
||||
messageIds: [],
|
||||
model: sdkSession.model,
|
||||
agent: sdkSession.agent,
|
||||
}
|
||||
|
||||
// Import messages if provided
|
||||
if (sdkSession.messages && Array.isArray(sdkSession.messages)) {
|
||||
for (const msg of sdkSession.messages) {
|
||||
const message: SessionMessage = {
|
||||
id: msg.id,
|
||||
sessionId: sdkSession.id,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
createdAt: msg.createdAt || now,
|
||||
updatedAt: msg.createdAt || now,
|
||||
status: "completed"
|
||||
}
|
||||
store.messages[msg.id] = message
|
||||
session.messageIds.push(msg.id)
|
||||
}
|
||||
}
|
||||
|
||||
store.sessions[sdkSession.id] = session
|
||||
imported++
|
||||
}
|
||||
|
||||
await this.saveStore(workspaceId)
|
||||
log.info({ workspaceId, imported, skipped }, "Imported sessions from SDK mode")
|
||||
return { imported, skipped }
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
@@ -165,6 +165,34 @@ export const nativeSessionApi = {
|
||||
return data.messages
|
||||
},
|
||||
|
||||
/**
|
||||
* Import sessions from SDK mode to Native mode
|
||||
*/
|
||||
async importSessions(workspaceId: string, sessions: Array<{
|
||||
id: string
|
||||
title?: string
|
||||
parentId?: string | null
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
model?: { providerId: string; modelId: string }
|
||||
agent?: string
|
||||
messages?: Array<{
|
||||
id: string
|
||||
role: "user" | "assistant" | "system" | "tool"
|
||||
content?: string
|
||||
createdAt?: number
|
||||
}>
|
||||
}>): Promise<{ success: boolean; imported: number; skipped: number }> {
|
||||
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/import`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessions })
|
||||
})
|
||||
if (!response.ok) throw new Error("Failed to import sessions")
|
||||
return response.json()
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send a prompt to the session and get a streaming response
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Message } from "../types/message"
|
||||
|
||||
import { instances } from "./instances"
|
||||
import { nativeSessionApi } from "../lib/lite-mode"
|
||||
import { needsMigration, migrateSessionsToNative, markMigrated, getExistingSdkSessions } from "./session-migration"
|
||||
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
||||
import { setSessionCompactionState } from "./session-compaction"
|
||||
import {
|
||||
@@ -389,6 +390,26 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
let responseData: any[] = []
|
||||
|
||||
if (isNative) {
|
||||
// Check if we need to migrate sessions from SDK mode
|
||||
if (needsMigration(instanceId)) {
|
||||
const existingSdkSessions = getExistingSdkSessions(instanceId)
|
||||
if (existingSdkSessions.length > 0) {
|
||||
log.info({ instanceId, count: existingSdkSessions.length }, "Migrating SDK sessions to native mode")
|
||||
const migrationData = existingSdkSessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
parentId: s.parentId,
|
||||
time: s.time,
|
||||
model: s.model,
|
||||
agent: s.agent
|
||||
}))
|
||||
const result = await migrateSessionsToNative(instanceId, migrationData)
|
||||
log.info({ instanceId, result }, "Migration completed")
|
||||
} else {
|
||||
markMigrated(instanceId)
|
||||
}
|
||||
}
|
||||
|
||||
const nativeSessions = await nativeSessionApi.listSessions(instanceId)
|
||||
responseData = nativeSessions.map(s => ({
|
||||
id: s.id,
|
||||
|
||||
125
packages/ui/src/stores/session-migration.ts
Normal file
125
packages/ui/src/stores/session-migration.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Session Migration - Handles importing sessions when switching between SDK and Native 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")
|
||||
|
||||
// Track which workspaces have already been migrated to prevent duplicate migrations
|
||||
const migratedWorkspaces = new Set<string>()
|
||||
|
||||
export interface MigrationResult {
|
||||
success: boolean
|
||||
imported: number
|
||||
skipped: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
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.time?.created,
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
Reference in New Issue
Block a user