Add automatic session migration when switching from SDK to Native mode
Some checks failed
Release Binaries / release (push) Has been cancelled

This commit is contained in:
Gemini AI
2025-12-27 11:13:43 +04:00
Unverified
parent eaf93e2924
commit 64c7fb8d47
6 changed files with 311 additions and 0 deletions

View 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": {}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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,

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