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
|
// Get messages for a session
|
||||||
app.get<{ Params: { workspaceId: string; sessionId: string } }>("/api/native/workspaces/:workspaceId/sessions/:sessionId/messages", async (request, reply) => {
|
app.get<{ Params: { workspaceId: string; sessionId: string } }>("/api/native/workspaces/:workspaceId/sessions/:sessionId/messages", async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -311,6 +311,74 @@ export class NativeSessionManager {
|
|||||||
const store = this.stores.get(workspaceId)
|
const store = this.stores.get(workspaceId)
|
||||||
return store ? Object.keys(store.sessions).length : 0
|
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
|
// Singleton instance
|
||||||
|
|||||||
@@ -165,6 +165,34 @@ export const nativeSessionApi = {
|
|||||||
return data.messages
|
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
|
* 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 { instances } from "./instances"
|
||||||
import { nativeSessionApi } from "../lib/lite-mode"
|
import { nativeSessionApi } from "../lib/lite-mode"
|
||||||
|
import { needsMigration, migrateSessionsToNative, markMigrated, getExistingSdkSessions } from "./session-migration"
|
||||||
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
||||||
import { setSessionCompactionState } from "./session-compaction"
|
import { setSessionCompactionState } from "./session-compaction"
|
||||||
import {
|
import {
|
||||||
@@ -389,6 +390,26 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
let responseData: any[] = []
|
let responseData: any[] = []
|
||||||
|
|
||||||
if (isNative) {
|
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)
|
const nativeSessions = await nativeSessionApi.listSessions(instanceId)
|
||||||
responseData = nativeSessions.map(s => ({
|
responseData = nativeSessions.map(s => ({
|
||||||
id: s.id,
|
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